diff --git a/.circleci/config.yml b/.circleci/config.yml index 84ddcdf26ea..22539912268 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ aliases: - &environment docker: # specify the version you desire here - - image: circleci/node:12.16.1-browsers + - image: cimg/node:16.20-browsers resource_class: xlarge # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images diff --git a/.eslintrc.js b/.eslintrc.js index fc3ad3afe66..f17c7a0063d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,12 +11,25 @@ module.exports = { node: { moduleDirectory: ['node_modules', './'] } + }, + 'jsdoc': { + mode: 'typescript', + tagNamePreference: { + 'tag constructor': 'constructor', + extends: 'extends', + method: 'method', + return: 'return', + } } }, - extends: 'standard', + extends: [ + 'standard', + 'plugin:jsdoc/recommended' + ], plugins: [ 'prebid', - 'import' + 'import', + 'jsdoc' ], globals: { 'BROWSERSTACK_USERNAME': false, @@ -29,6 +42,7 @@ module.exports = { sourceType: 'module', ecmaVersion: 2018, }, + ignorePatterns: ['libraries/creative-renderer*'], rules: { 'comma-dangle': 'off', @@ -46,6 +60,24 @@ module.exports = { 'no-undef': 2, 'no-useless-escape': 'off', 'no-console': 'error', + 'jsdoc/check-types': 'off', + 'jsdoc/newline-after-description': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-param-name': 'off', + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-property': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-property-name': 'off', + 'jsdoc/require-property-type': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-returns-check': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/require-yields': 'off', + 'jsdoc/require-yields-check': 'off', + 'jsdoc/tag-lines': 'off' }, overrides: Object.keys(allowedModules).map((key) => ({ files: key + '/**/*.js', diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 584f6f8894a..3bee8f7c947 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -70,4 +70,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index 69cf4c5fc7f..b5c59c85160 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a13237f1290..a14e12664b6 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 with: config-name: release-drafter.yml env: diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 45ca30a7a3d..9deac9963fb 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -55,7 +55,6 @@ Follow steps above for general review process. In addition, please verify the fo - 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 bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): - bcat, battr, badv diff --git a/README.md b/README.md index 58007519b15..e6d25a5cb5a 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,12 @@ gulp test-coverage gulp view-coverage ``` +Local end-to-end testing can be done with: + +```bash +gulp e2e-test --local +``` + For Prebid.org members with access to BrowserStack, additional end-to-end testing can be done with: ```bash diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index 45f4e6c7dc5..016e3e71cfc 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -9,7 +9,7 @@ ## Release Schedule -We aim to push a new release of Prebid.js every week on Tuesday. +We aim to push a new release of Prebid.js each week barring any unforseen circumstances or in observance of holidays. While the releases will be available immediately for those using direct Git access, it will be about a week before the Prebid Org [Download Page](https://docs.prebid.org/download.html) will be updated. diff --git a/allowedModules.js b/allowedModules.js index 3e6e3039fa2..75ad4141a6c 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,22 +1,18 @@ -const sharedWhiteList = [ -]; - module.exports = { 'modules': [ - ...sharedWhiteList, 'criteo-direct-rsa-validate', 'crypto-js', 'live-connect' // Maintained by LiveIntent : https://github.com/liveintent-berlin/live-connect/ ], 'src': [ - ...sharedWhiteList, 'fun-hooks/no-eval', 'just-clone', 'dlv', 'dset' ], 'libraries': [ - ...sharedWhiteList // empty for now, but keep it to enable linting + ], + 'creative': [ ] }; diff --git a/creative/README.md b/creative/README.md new file mode 100644 index 00000000000..76f0be833e3 --- /dev/null +++ b/creative/README.md @@ -0,0 +1,44 @@ +## Dynamic creative renderers + +The contents of this directory are compiled separately from the rest of Prebid, and intended to be dynamically injected +into creative frames: + +- `crossDomain.js` (compiled into `build/creative/creative.js`, also exposed in `integrationExamples/gpt/x-domain/creative.html`) + is the logic that should be statically set up in the creative. +- At build time, each folder under 'renderers' is compiled into a source string made available from a corresponding +`creative-renderer-*` library. These libraries are committed in source so that they are available to NPM consumers. +- At render time, Prebid passes the appropriate renderer's source string to the remote creative, which then runs it. + +The goal is to have a creative script that is as simple, lightweight, and unchanging as possible, but still allow the possibility +of complex or frequently updated rendering logic. Compared to the approach taken by [PUC](https://github.com/prebid/prebid-universal-creative), this: + +- should perform marginally better: the creative only runs logic that is pertinent (for example, it sees native logic only on native bids); +- avoids the problem of synchronizing deployments when the rendering logic is updated (see https://github.com/prebid/prebid-universal-creative/issues/187), since it's bundled together with the rest of Prebid; +- is easier to embed directly in the creative (saving a network call), since the static "shell" is designed to change as infrequently as possible; +- allows the same rendering logic to be used both in remote (cross-domain) and local (`pbjs.renderAd`) frames, since it's directly available to Prebid; +- requires Prebid.js - meaning it does not support AMP/App/Mobile (but it's still possible for something like PUC to run the same dynamic renderers + when it receives them from Prebid, and fall back to separate AMP/App/Mobile logic otherwise). + +### Renderer interface + +A creative renderer (not related to other types of renderers in the codebase) is a script that exposes a global `window.render` function: + +```javascript +window.render = function(data, {mkFrame, sendMessage}, win) { ... } +``` + +where: + + - `data` is rendering data about the winning bid, and varies depending on the bid type - see `getRenderingData` in `adRendering.js`; + - `mkFrame(document, attributes)` is a utility that creates a frame with the given attributes and convenient defaults (no border, margin, and scrolling); + - `sendMessage(messageType, payload)` is the mechanism by which the renderer/creative can communicate back with Prebid - see `creativeMessageHandler` in `adRendering.js`; + - `win` is the window to render into; note that this is not the same window that runs the renderer. + +The function may return a promise; if it does and the promise rejects, or if the function throws, an AD_RENDER_FAILED event is emitted in Prebid. Otherwise an AD_RENDER_SUCCEEDED is fired +when the promise resolves (or when `render` returns anything other than a promise). + +### Renderer development + +Since renderers are compiled into source, they use production settings even during development builds. You can toggle this with +the `--creative-dev` CLI option (e.g., `gulp serve-fast --creative-dev`), which disables the minifier and generates source maps; if you do, take care +to not commit the resulting `creative-renderer-*` libraries (or run a normal build before you do). diff --git a/creative/constants.js b/creative/constants.js new file mode 100644 index 00000000000..6bb92cfe3c2 --- /dev/null +++ b/creative/constants.js @@ -0,0 +1,9 @@ +// eslint-disable-next-line prebid/validate-imports +import CONSTANTS from '../src/constants.json'; + +export const MESSAGE_REQUEST = CONSTANTS.MESSAGES.REQUEST; +export const MESSAGE_RESPONSE = CONSTANTS.MESSAGES.RESPONSE; +export const MESSAGE_EVENT = CONSTANTS.MESSAGES.EVENT; +export const EVENT_AD_RENDER_FAILED = CONSTANTS.EVENTS.AD_RENDER_FAILED; +export const EVENT_AD_RENDER_SUCCEEDED = CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED; +export const ERROR_EXCEPTION = CONSTANTS.AD_RENDER_FAILED_REASON.EXCEPTION; diff --git a/creative/crossDomain.js b/creative/crossDomain.js new file mode 100644 index 00000000000..a851885bfc0 --- /dev/null +++ b/creative/crossDomain.js @@ -0,0 +1,92 @@ +import { + ERROR_EXCEPTION, + EVENT_AD_RENDER_FAILED, EVENT_AD_RENDER_SUCCEEDED, + MESSAGE_EVENT, + MESSAGE_REQUEST, + MESSAGE_RESPONSE +} from './constants.js'; + +const mkFrame = (() => { + const DEFAULTS = { + frameBorder: 0, + scrolling: 'no', + marginHeight: 0, + marginWidth: 0, + topMargin: 0, + leftMargin: 0, + allowTransparency: 'true', + }; + return (doc, attrs) => { + const frame = doc.createElement('iframe'); + Object.entries(Object.assign({}, attrs, DEFAULTS)) + .forEach(([k, v]) => frame.setAttribute(k, v)); + return frame; + }; +})(); + +export function renderer(win) { + return function ({adId, pubUrl, clickUrl}) { + const pubDomain = new URL(pubUrl, window.location).origin; + + function sendMessage(type, payload, responseListener) { + const channel = new MessageChannel(); + channel.port1.onmessage = guard(responseListener); + win.parent.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, [channel.port2]); + } + + function onError(e) { + sendMessage(MESSAGE_EVENT, { + event: EVENT_AD_RENDER_FAILED, + info: { + reason: e?.reason || ERROR_EXCEPTION, + message: e?.message + } + }); + // eslint-disable-next-line no-console + e?.stack && console.error(e); + } + + function guard(fn) { + return function () { + try { + return fn.apply(this, arguments); + } catch (e) { + onError(e); + } + }; + } + + function onMessage(ev) { + let data; + try { + data = JSON.parse(ev.data); + } catch (e) { + return; + } + if (data.message === MESSAGE_RESPONSE && data.adId === adId) { + const renderer = mkFrame(win.document, { + width: 0, + height: 0, + style: 'display: none', + srcdoc: `` + }); + renderer.onload = guard(function () { + const W = renderer.contentWindow; + // NOTE: on Firefox, `Promise.resolve(P)` or `new Promise((resolve) => resolve(P))` + // does not appear to work if P comes from another frame + W.Promise.resolve(W.render(data, {sendMessage, mkFrame}, win)).then( + () => sendMessage(MESSAGE_EVENT, {event: EVENT_AD_RENDER_SUCCEEDED}), + onError + ) + }); + win.document.body.appendChild(renderer); + } + } + + sendMessage(MESSAGE_REQUEST, { + options: {clickUrl} + }, onMessage); + }; +} + +window.pbRender = renderer(window); diff --git a/creative/renderers/display/constants.js b/creative/renderers/display/constants.js new file mode 100644 index 00000000000..d291c79bb34 --- /dev/null +++ b/creative/renderers/display/constants.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line prebid/validate-imports +import CONSTANTS from '../../../src/constants.json'; + +export const ERROR_NO_AD = CONSTANTS.AD_RENDER_FAILED_REASON.NO_AD; diff --git a/creative/renderers/display/renderer.js b/creative/renderers/display/renderer.js new file mode 100644 index 00000000000..e031679b116 --- /dev/null +++ b/creative/renderers/display/renderer.js @@ -0,0 +1,21 @@ +import {ERROR_NO_AD} from './constants.js'; + +export function render({ad, adUrl, width, height}, {mkFrame}, win) { + if (!ad && !adUrl) { + throw { + reason: ERROR_NO_AD, + message: 'Missing ad markup or URL' + }; + } else { + const doc = win.document; + const attrs = {width, height}; + if (adUrl && !ad) { + attrs.src = adUrl; + } else { + attrs.srcdoc = ad; + } + doc.body.appendChild(mkFrame(doc, attrs)); + } +} + +window.render = render; diff --git a/creative/renderers/native/constants.js b/creative/renderers/native/constants.js new file mode 100644 index 00000000000..ac20275fca8 --- /dev/null +++ b/creative/renderers/native/constants.js @@ -0,0 +1,14 @@ +// eslint-disable-next-line prebid/validate-imports +import CONSTANTS from '../../../src/constants.json'; + +export const MESSAGE_NATIVE = CONSTANTS.MESSAGES.NATIVE; +export const ACTION_RESIZE = 'resizeNativeHeight'; +export const ACTION_CLICK = 'click'; +export const ACTION_IMP = 'fireNativeImpressionTrackers'; + +export const ORTB_ASSETS = { + title: 'text', + data: 'value', + img: 'url', + video: 'vasttag' +} diff --git a/creative/renderers/native/renderer.js b/creative/renderers/native/renderer.js new file mode 100644 index 00000000000..5cc8f100108 --- /dev/null +++ b/creative/renderers/native/renderer.js @@ -0,0 +1,88 @@ +import {ACTION_CLICK, ACTION_IMP, ACTION_RESIZE, MESSAGE_NATIVE, ORTB_ASSETS} from './constants.js'; + +export function getReplacer(adId, {assets = [], ortb, nativeKeys = {}}) { + const assetValues = Object.fromEntries((assets).map(({key, value}) => [key, value])); + let repl = Object.fromEntries( + Object.entries(nativeKeys).flatMap(([name, key]) => { + const value = assetValues.hasOwnProperty(name) ? assetValues[name] : undefined; + return [ + [`##${key}##`, value], + [`${key}:${adId}`, value] + ]; + }) + ); + if (ortb) { + Object.assign(repl, + { + '##hb_native_linkurl##': ortb.link?.url, + '##hb_native_privacy##': ortb.privacy + }, + Object.fromEntries( + (ortb.assets || []).flatMap(asset => { + const type = Object.keys(ORTB_ASSETS).find(type => asset[type]); + return [ + type && [`##hb_native_asset_id_${asset.id}##`, asset[type][ORTB_ASSETS[type]]], + asset.link?.url && [`##hb_native_asset_link_id_${asset.id}##`, asset.link.url] + ].filter(e => e); + }) + ) + ); + } + repl = Object.entries(repl).concat([[/##hb_native_asset_(link_)?id_\d+##/g]]); + + return function (template) { + return repl.reduce((text, [pattern, value]) => text.replaceAll(pattern, value || ''), template); + }; +} + +function loadScript(url, doc) { + return new Promise((resolve, reject) => { + const script = doc.createElement('script'); + script.onload = resolve; + script.onerror = reject; + script.src = url; + doc.body.appendChild(script); + }); +} + +export function getAdMarkup(adId, nativeData, replacer, win, load = loadScript) { + const {rendererUrl, assets, ortb, adTemplate} = nativeData; + const doc = win.document; + if (rendererUrl) { + return load(rendererUrl, doc).then(() => { + if (typeof win.renderAd !== 'function') { + throw new Error(`Renderer from '${rendererUrl}' does not define renderAd()`); + } + const payload = assets || []; + payload.ortb = ortb; + return win.renderAd(payload); + }); + } else { + return Promise.resolve(replacer(adTemplate ?? doc.body.innerHTML)); + } +} + +export function render({adId, native}, {sendMessage}, win, getMarkup = getAdMarkup) { + const {head, body} = win.document; + const resize = () => sendMessage(MESSAGE_NATIVE, { + action: ACTION_RESIZE, + height: body.offsetHeight, + width: body.offsetWidth + }); + const replacer = getReplacer(adId, native); + head && (head.innerHTML = replacer(head.innerHTML)); + return getMarkup(adId, native, replacer, win).then(markup => { + body.innerHTML = markup; + if (typeof win.postRenderAd === 'function') { + win.postRenderAd({adId, ...native}); + } + win.document.querySelectorAll('.pb-click').forEach(el => { + const assetId = el.getAttribute('hb_native_asset_id'); + el.addEventListener('click', () => sendMessage(MESSAGE_NATIVE, {action: ACTION_CLICK, assetId})); + }); + sendMessage(MESSAGE_NATIVE, {action: ACTION_IMP}); + win.document.readyState === 'complete' ? resize() : win.onload = resize; + }); +} + +window.render = render; diff --git a/features.json b/features.json index ccb2166a05f..4d8377cda7d 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,5 @@ [ "NATIVE", - "VIDEO" + "VIDEO", + "UID2_CSTG" ] diff --git a/gulpfile.js b/gulpfile.js index 09de874e389..17c421f4dc1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -25,6 +25,8 @@ const path = require('path'); const execa = require('execa'); const {minify} = require('terser'); const Vinyl = require('vinyl'); +const wrap = require('gulp-wrap'); +const rename = require('gulp-rename'); var prebid = require('./package.json'); var port = 9999; @@ -52,6 +54,18 @@ function clean() { .pipe(gulpClean()); } +function requireNodeVersion(version) { + return (done) => { + const [major] = process.versions.node.split('.'); + + if (major < version) { + throw new Error(`This task requires Node v${version}`) + } + + done(); + } +} + // Dependant task for building postbid. It escapes postbid-config file. function escapePostbidConfig() { gulp.src('./integrationExamples/postbid/oas/postbid-config.js') @@ -71,12 +85,13 @@ function lint(done) { 'src/**/*.js', 'modules/**/*.js', 'libraries/**/*.js', + 'creative/**/*.js', 'test/**/*.js', 'plugins/**/*.js', '!plugins/**/node_modules/**', './*.js' ], { base: './' }) - .pipe(gulpif(argv.nolintfix, eslint(), eslint({ fix: true }))) + .pipe(eslint({ fix: !argv.nolintfix, quiet: !(typeof argv.lintWarnings === 'boolean' ? argv.lintWarnings : true) })) .pipe(eslint.format('stylish')) .pipe(eslint.failAfterError()) .pipe(gulpif(isFixed, gulp.dest('./'))); @@ -158,6 +173,39 @@ function makeWebpackPkg(extraConfig = {}) { } } +function buildCreative(mode = 'production') { + const opts = {mode}; + if (mode === 'development') { + opts.devtool = 'inline-source-map' + } + return function() { + return gulp.src(['**/*']) + .pipe(webpackStream(Object.assign(require('./webpack.creative.js'), opts))) + .pipe(gulp.dest('build/creative')) + } +} + +function updateCreativeRenderers() { + return gulp.src(['build/creative/renderers/**/*']) + .pipe(wrap('// this file is autogenerated, see creative/README.md\nexport const RENDERER = <%= JSON.stringify(contents.toString()) %>')) + .pipe(rename(function (path) { + return { + dirname: `creative-renderer-${path.basename}`, + basename: 'renderer', + extname: '.js' + } + })) + .pipe(gulp.dest('libraries')) +} + +function updateCreativeExample(cb) { + const CREATIVE_EXAMPLE = 'integrationExamples/gpt/x-domain/creative.html'; + const root = require('node-html-parser').parse(fs.readFileSync(CREATIVE_EXAMPLE)); + root.querySelectorAll('script')[0].textContent = fs.readFileSync('build/creative/creative.js') + fs.writeFileSync(CREATIVE_EXAMPLE, root.toString()) + cb(); +} + function getModulesListToAddInBanner(modules) { if (!modules || modules.length === helpers.getModuleNames().length) { return 'All available modules for this version.' @@ -247,8 +295,7 @@ function bundle(dev, moduleArr) { [coreFile].concat(moduleFiles).map(name => path.basename(name)).forEach((file) => { (depGraph[file] || []).forEach((dep) => dependencies.add(helpers.getBuiltPath(dev, dep))); }); - - const entries = [coreFile].concat(Array.from(dependencies), moduleFiles); + const entries = _.uniq([coreFile].concat(Array.from(dependencies), moduleFiles)); var outputFileName = argv.bundleName ? argv.bundleName : 'prebid.js'; @@ -279,7 +326,7 @@ function bundle(dev, moduleArr) { // If --notest is given, it will immediately skip the test task (useful for developing changes with `gulp serve --notest`) function testTaskMaker(options = {}) { - ['watch', 'e2e', 'file', 'browserstack', 'notest'].forEach(opt => { + ['watch', 'file', 'browserstack', 'notest'].forEach(opt => { options[opt] = options.hasOwnProperty(opt) ? options[opt] : argv[opt]; }) @@ -288,22 +335,6 @@ function testTaskMaker(options = {}) { return function test(done) { if (options.notest) { done(); - } else if (options.e2e) { - const integ = startIntegServer(); - startLocalServer(); - runWebdriver(options) - .then(stdout => { - // kill fake server - integ.kill('SIGINT'); - done(); - process.exit(0); - }) - .catch(err => { - // kill fake server - integ.kill('SIGINT'); - done(new Error(`Tests failed with error: ${err}`)); - process.exit(1); - }); } else { runKarma(options, done) } @@ -312,10 +343,34 @@ function testTaskMaker(options = {}) { const test = testTaskMaker(); +function e2eTestTaskMaker() { + return function test(done) { + const integ = startIntegServer(); + startLocalServer(); + runWebdriver({}) + .then(stdout => { + // kill fake server + integ.kill('SIGINT'); + done(); + process.exit(0); + }) + .catch(err => { + // kill fake server + integ.kill('SIGINT'); + done(new Error(`Tests failed with error: ${err}`)); + process.exit(1); + }); + } +} + function runWebdriver({file}) { process.env.TEST_SERVER_HOST = argv.host || 'localhost'; + + let local = argv.local || false; + + let wdioConfFile = local === true ? 'wdio.local.conf.js' : 'wdio.conf.js'; let wdioCmd = path.join(__dirname, 'node_modules/.bin/wdio'); - let wdioConf = path.join(__dirname, 'wdio.conf.js'); + let wdioConf = path.join(__dirname, wdioConfFile); let wdioOpts; if (file) { @@ -405,6 +460,9 @@ function watchTaskMaker(options = {}) { return function watch(done) { var mainWatcher = gulp.watch([ 'src/**/*.js', + 'libraries/**/*.js', + '!libraries/creative-renderer-*/**/*.js', + 'creative/**/*.js', 'modules/**/*.js', ].concat(options.alsoWatch)); @@ -426,8 +484,11 @@ gulp.task(clean); gulp.task(escapePostbidConfig); -gulp.task('build-bundle-dev', gulp.series(makeDevpackPkg, gulpBundle.bind(null, true))); -gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg(), gulpBundle.bind(null, false))); +gulp.task('build-creative-dev', gulp.series(buildCreative(argv.creativeDev ? 'development' : 'production'), updateCreativeRenderers)); +gulp.task('build-creative-prod', gulp.series(buildCreative(), updateCreativeRenderers)); + +gulp.task('build-bundle-dev', gulp.series('build-creative-dev', makeDevpackPkg, gulpBundle.bind(null, true))); +gulp.task('build-bundle-prod', gulp.series('build-creative-prod', makeWebpackPkg(), gulpBundle.bind(null, false))); // build-bundle-verbose - prod bundle except names and comments are preserved. Use this to see the effects // of dead code elimination. gulp.task('build-bundle-verbose', gulp.series(makeWebpackPkg({ @@ -450,14 +511,14 @@ gulp.task('build-bundle-verbose', gulp.series(makeWebpackPkg({ // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('test-only', test); gulp.task('test-all-features-disabled', testTaskMaker({disableFeatures: require('./features.json'), oneBrowser: 'chrome', watch: false})); -gulp.task('test', gulp.series(clean, lint, gulp.series('test-all-features-disabled', 'test-only'))); +gulp.task('test', gulp.series(clean, lint, 'test-all-features-disabled', 'test-only')); gulp.task('test-coverage', gulp.series(clean, testCoverage)); gulp.task(viewCoverage); gulp.task('coveralls', gulp.series('test-coverage', coveralls)); -gulp.task('build', gulp.series(clean, 'build-bundle-prod')); +gulp.task('build', gulp.series(clean, 'build-bundle-prod', updateCreativeExample)); gulp.task('build-postbid', gulp.series(escapePostbidConfig, buildPostbid)); gulp.task('serve', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, test))); @@ -469,8 +530,9 @@ gulp.task('serve-e2e-dev', gulp.series(clean, 'build-bundle-dev', gulp.parallel( gulp.task('default', gulp.series(clean, 'build-bundle-prod')); -gulp.task('e2e-test-only', () => runWebdriver({file: argv.file})); -gulp.task('e2e-test', gulp.series(clean, 'build-bundle-prod', testTaskMaker({e2e: true}))); +gulp.task('e2e-test-only', gulp.series(requireNodeVersion(16), () => runWebdriver({file: argv.file}))); +gulp.task('e2e-test', gulp.series(requireNodeVersion(16), clean, 'build-bundle-prod', e2eTestTaskMaker())); + // other tasks gulp.task(bundleToStdout); gulp.task('bundle', gulpBundle.bind(null, false)); // used for just concatenating pre-built files with no build step diff --git a/integrationExamples/gpt/adUnitFloors.html b/integrationExamples/gpt/adUnitFloors.html index bb48a20ef78..a80e1b2380b 100644 --- a/integrationExamples/gpt/adUnitFloors.html +++ b/integrationExamples/gpt/adUnitFloors.html @@ -109,4 +109,3 @@
Div-1
- diff --git a/integrationExamples/gpt/adnuntius_example.html b/integrationExamples/gpt/adnuntius_example.html new file mode 100644 index 00000000000..b61c4e0674e --- /dev/null +++ b/integrationExamples/gpt/adnuntius_example.html @@ -0,0 +1,95 @@ + + + + + + + +

Adnuntius Prebid Adaptor Test

+
Ad Slot 1
+ + +
+ +
+ + + diff --git a/integrationExamples/gpt/azerionedgeRtdProvider_example.html b/integrationExamples/gpt/azerionedgeRtdProvider_example.html new file mode 100644 index 00000000000..880fe5ed706 --- /dev/null +++ b/integrationExamples/gpt/azerionedgeRtdProvider_example.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + +

Azerion Edge RTD

+ +
+ +
+ + Segments: +
+ + diff --git a/integrationExamples/gpt/contxtfulRtdProvider_example.html b/integrationExamples/gpt/contxtfulRtdProvider_example.html new file mode 100644 index 00000000000..29284de81a2 --- /dev/null +++ b/integrationExamples/gpt/contxtfulRtdProvider_example.html @@ -0,0 +1,91 @@ + + + + + + + + + +

Contxtful RTD Provider

+
+ + + + \ No newline at end of file diff --git a/integrationExamples/gpt/cstg_example.html b/integrationExamples/gpt/cstg_example.html new file mode 100644 index 00000000000..8ca049a0ed0 --- /dev/null +++ b/integrationExamples/gpt/cstg_example.html @@ -0,0 +1,317 @@ + + + + + UID2 and EUID Prebid.js Integration Example + + + + +

UID2 and EUID Prebid.js Integration Examples

+ +

+ This example demonstrates how a content publisher can integrate with UID2 and Prebid.js using the UID2 Client-Side Integration Guide for Prebid.js, which includes generating UID2 tokens within the browser.
+ This example is configured to hit endpoints at https://operator-integ.uidapi.com. Calls to this endpoint will be rejected if made from localhost.
+ A working sample subscription_id and client_key are declared in the javascript. Please override them in set[Uid2|Euid]Config() to test with your own CSTG credentials.
+ Note Generation of UID2 after EUID will fail due to consent settings on pbjs config. + +

+ +

UID2 Example

+
+ + + + + + + + + + + + + +
CSTG Subscription Id:
CSTG Public Key:
Email Address (DII): + +
+ +
+
+ + + + + + + + + +
Ready for Targeted Advertising:
UID2 Advertising Token:
+
+ +
+
+
+

EUID Example

+
+ + + + + + + + + + + + + +
CSTG Subscription Id:
CSTG Public Key:
Email Address (DII): + +
+ +
+
+ + + + + + + + + +
Ready for Targeted Advertising:
EUID Advertising Token:
+
+ +
+ + diff --git a/integrationExamples/gpt/fledge_example.html b/integrationExamples/gpt/fledge_example.html index 5059e03daef..5a6ab7a5fef 100644 --- a/integrationExamples/gpt/fledge_example.html +++ b/integrationExamples/gpt/fledge_example.html @@ -44,15 +44,11 @@ pbjs.que.push(function() { pbjs.setConfig({ - fledgeForGpt: { - enabled: true - } - }); - - pbjs.setBidderConfig({ - bidders: ['openx'], - config: { - fledgeEnabled: true + paapi: { + enabled: true, + gpt: { + autoconfig: false + } } }); @@ -69,6 +65,7 @@ googletag.cmd.push(function() { pbjs.que.push(function() { pbjs.setTargetingForGPTAsync(); + pbjs.setPAAPIConfigForGPT(); googletag.pubads().refresh(); }); }); diff --git a/integrationExamples/gpt/gdpr_hello_world.html b/integrationExamples/gpt/gdpr_hello_world.html index c62569cfc4f..e23a866d4fd 100644 --- a/integrationExamples/gpt/gdpr_hello_world.html +++ b/integrationExamples/gpt/gdpr_hello_world.html @@ -54,8 +54,7 @@ pbjs.setConfig({ consentManagement: { cmpApi: 'iab', - timeout: 5000, - allowAuctionWithoutConsent: true + timeout: 5000 }, pubcid: { enable: false diff --git a/integrationExamples/gpt/growthcode.html b/integrationExamples/gpt/growthcode.html index d8ad6c4a5af..35de2b710ad 100644 --- a/integrationExamples/gpt/growthcode.html +++ b/integrationExamples/gpt/growthcode.html @@ -56,20 +56,11 @@ provider: 'growthCodeAnalytics', options: { pid: 'TEST01', + //url: 'http://localhost:8080/v3/pb/analytics', trackEvents: [ - 'auctionInit', 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'setTargeting', - 'requestBids', - 'addAdUnits', - 'noBid', 'bidWon', - 'bidderDone'] + ] } }); pbjs.setConfig({ @@ -80,7 +71,7 @@ auctionDelay: 1000, dataProviders: [{ name: 'growthCodeRtd', - waitForIt: true, + waitForIt: false, params: { pid: 'TEST01', } diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index 47ba5b8f18a..03a2356f0ef 100644 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -8,6 +8,7 @@ --> + @@ -19,9 +20,10 @@ code: 'div-gpt-ad-1460505748561-0', mediaTypes: { banner: { - sizes: [[300, 250], [300,600]], + sizes: [[300, 250]], } }, + // Replace this object to test a new Adapter! bids: [{ bidder: 'appnexus', @@ -40,12 +42,13 @@ - +

Prebid.js Test

+
Div-1
+
+ +
+ \ No newline at end of file diff --git a/integrationExamples/gpt/pbjs_video_adUnit.html b/integrationExamples/gpt/pbjs_video_adUnit.html deleted file mode 100644 index 080ca9be142..00000000000 --- a/integrationExamples/gpt/pbjs_video_adUnit.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - Prebid.js video adUnit example - - - - - - - - - - - -
- -
- - - - - diff --git a/integrationExamples/gpt/prebidServer_example.html b/integrationExamples/gpt/prebidServer_example.html index f23554369bc..ded50777ad2 100644 --- a/integrationExamples/gpt/prebidServer_example.html +++ b/integrationExamples/gpt/prebidServer_example.html @@ -33,31 +33,41 @@ pbjs.que.push(function() { var adUnits = [{ - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - } + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [600, 500] + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 12883451 } - ] - }]; + } + ] + }]; + pbjs.bidderSettings = { + appnexus: { + bidCpmAdjustment: function () { + return 10; + } + } + } pbjs.setConfig({ bidderTimeout: 3000, s2sConfig : { accountId : '1', enabled : true, //default value set to false - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders : ['appnexus'], timeout : 1000, //default value is 1000 adapter : 'prebidServer', //if we have any other s2s adapter, default value is s2s + }, + ortb2: { + test: 1 } }); diff --git a/integrationExamples/gpt/prebidServer_fledge_example.html b/integrationExamples/gpt/prebidServer_fledge_example.html index 8523c0f2920..eb2fc438997 100644 --- a/integrationExamples/gpt/prebidServer_fledge_example.html +++ b/integrationExamples/gpt/prebidServer_fledge_example.html @@ -50,7 +50,7 @@ s2sConfig: [{ accountId : '1', enabled : true, - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders : ['openx'], timeout : 1500, adapter : 'prebidServer' diff --git a/integrationExamples/gpt/prebidServer_native_example.html b/integrationExamples/gpt/prebidServer_native_example.html index c590f0bcee5..a5fb0ffa894 100644 --- a/integrationExamples/gpt/prebidServer_native_example.html +++ b/integrationExamples/gpt/prebidServer_native_example.html @@ -114,7 +114,7 @@ s2sConfig: { accountId: '1', enabled: true, //default value set to false - bidders: ['appnexus'], + bidders: ['appnexuspsp'], timeout: 1000, //default value is 1000 adapter: 'prebidServer', //if we have any other s2s adapter, default value is s2s endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' diff --git a/integrationExamples/gpt/publir_hello_world.html b/integrationExamples/gpt/publir_hello_world.html new file mode 100644 index 00000000000..4763525408d --- /dev/null +++ b/integrationExamples/gpt/publir_hello_world.html @@ -0,0 +1,84 @@ + + + + + Prebid.js Banner gpt Example + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/raynRtdProvider_example.html b/integrationExamples/gpt/raynRtdProvider_example.html new file mode 100644 index 00000000000..2d43c37513a --- /dev/null +++ b/integrationExamples/gpt/raynRtdProvider_example.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + +

Rayn RTD Prebid

+ +
+ +
+ + Rayn Segments: +
+ + diff --git a/integrationExamples/gpt/tpmn_example.html b/integrationExamples/gpt/tpmn_example.html new file mode 100644 index 00000000000..f215181c7e0 --- /dev/null +++ b/integrationExamples/gpt/tpmn_example.html @@ -0,0 +1,168 @@ + + + + + Prebid.js Banner Example + + + + + + + + + + +

Prebid.js TPMN Banner Example

+ +
+

Prebid.js TPMN Video Example

+
+ +
+
+
+ diff --git a/integrationExamples/gpt/tpmn_serverless_example.html b/integrationExamples/gpt/tpmn_serverless_example.html new file mode 100644 index 00000000000..0acaefbeb9c --- /dev/null +++ b/integrationExamples/gpt/tpmn_serverless_example.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + +

Ad Serverless Test Page

+ + +
+
+ + diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index 2216d0ed6ae..bf2bd5f3fad 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -1,105 +1,13 @@ - -} - - -function requestAdFromPrebid() { - const message = JSON.stringify({ - message: 'Prebid Request', - adId - }); - const channel = new MessageChannel(); - channel.port1.onmessage = renderAd; - window.parent.postMessage(message, publisherDomain, [channel.port2]); -} - -function listenAdFromPrebid() { - window.addEventListener('message', receiveMessage, false); -} - -listenAdFromPrebid(); -requestAdFromPrebid(); + diff --git a/integrationExamples/noadserver/jwplayerBidAdapter_sample.html b/integrationExamples/noadserver/jwplayerBidAdapter_sample.html new file mode 100644 index 00000000000..f8b15af64a2 --- /dev/null +++ b/integrationExamples/noadserver/jwplayerBidAdapter_sample.html @@ -0,0 +1,75 @@ + + + + + + + + + + + diff --git a/integrationExamples/noadserver/native_noadserver.html b/integrationExamples/noadserver/native_noadserver.html new file mode 100755 index 00000000000..356c559b86f --- /dev/null +++ b/integrationExamples/noadserver/native_noadserver.html @@ -0,0 +1,173 @@ + + + + + + + + + + + + + +

Prebid native - no ad server

+
+
+ +
+
+ + + + diff --git a/integrationExamples/noadserver/native_renderer/custom_renderer.html b/integrationExamples/noadserver/native_renderer/custom_renderer.html new file mode 100644 index 00000000000..8ecfe40df53 --- /dev/null +++ b/integrationExamples/noadserver/native_renderer/custom_renderer.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + +

Prebid Native w/custom renderer

+
+
+ +
+
+ + + + diff --git a/integrationExamples/noadserver/native_renderer/renderer.js b/integrationExamples/noadserver/native_renderer/renderer.js new file mode 100644 index 00000000000..d1c754f20b7 --- /dev/null +++ b/integrationExamples/noadserver/native_renderer/renderer.js @@ -0,0 +1,69 @@ +window.renderAd = function (data) { + data = Object.fromEntries(data.map(({key, value}) => [key, value])); + return ` + +
+
+
+

+ ${data.title} +

+
+
+ ${data.sponsoredBy} +
+
+
`; +}; diff --git a/integrationExamples/gpt/jwplayerRtdProvider_example.html b/integrationExamples/realTimeData/jwplayerRtdProvider_example.html similarity index 51% rename from integrationExamples/gpt/jwplayerRtdProvider_example.html rename to integrationExamples/realTimeData/jwplayerRtdProvider_example.html index 41c27b70ece..f3f0c64fb1a 100644 --- a/integrationExamples/gpt/jwplayerRtdProvider_example.html +++ b/integrationExamples/realTimeData/jwplayerRtdProvider_example.html @@ -1,8 +1,10 @@ + + /* Paste JW Player script tag here */ + - JW Player RTD Provider Example + function renderHighestBid() { + const highestBids = pbjs.getHighestCpmBids('video-ad-unit'); + const highestBid = highestBids && highestBids.length && highestBids[0]; + if (!highestBid) { + return; + } - -
Div-1
-
- -
diff --git a/integrationExamples/topics/topics-server.js b/integrationExamples/topics/topics-server.js new file mode 100644 index 00000000000..0d248e5557c --- /dev/null +++ b/integrationExamples/topics/topics-server.js @@ -0,0 +1,72 @@ +// This is an example of a server-side endpoint that is utilizing the Topics API header functionality. +// Note: This test endpoint requires the following to run: node.js, npm, express, cors, body-parser + +const bodyParser = require('body-parser'); +const cors = require('cors'); +const express = require('express'); + +const port = process.env.PORT || 3000; + +const app = express(); +app.use(cors()); +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); +app.use(bodyParser.json()); +app.use(express.static('public')); +app.set('port', port); + +const listener = app.listen(port, () => { + const host = + listener.address().address === '::' + ? 'http://localhost' + : 'http://' + listener.address().address; + // eslint-disable-next-line no-console + console.log( + `${__filename} is listening on ${host}:${listener.address().port}\n` + ); +}); + +app.get('*', (req, res) => { + res.setHeader('Observe-Browsing-Topics', '?1'); + + const resData = { + segment: { + domain: req.hostname, + topics: generateTopicArrayFromHeader(req.headers['sec-browsing-topics']), + bidder: req.query['bidder'], + }, + date: Date.now(), + }; + + res.json(resData); +}); + +const generateTopicArrayFromHeader = (topicString) => { + const result = []; + const topicArray = topicString.split(', '); + if (topicArray.length > 1) { + topicArray.pop(); + topicArray.map((topic) => { + const topicId = topic.split(';')[0]; + const versionsString = topic.split(';')[1].split('=')[1]; + const [config, taxonomy, model] = versionsString.split(':'); + const numTopicsWithSameVersions = topicId + .substring(1, topicId.length - 1) + .split(' '); + + numTopicsWithSameVersions.map((tpId) => { + result.push({ + topic: tpId, + version: versionsString, + configVersion: config, + taxonomyVersion: taxonomy, + modelVersion: model, + }); + }); + }); + } + return result; +}; diff --git a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html index 6aa99678354..d0b261043e4 100644 --- a/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/jwplayer/bidMarkedAsUsed.html @@ -20,6 +20,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', @@ -37,7 +44,7 @@ divId: 'player', vendorCode: 1, // jwplayer vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', diff --git a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html index 620f891fa50..c40af32cac2 100644 --- a/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html +++ b/integrationExamples/videoModule/jwplayer/bidRequestScheduling.html @@ -20,7 +20,7 @@ divId: 'player', vendorCode: 1, // JW Player vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', @@ -50,12 +50,17 @@ mediationLayerAdServer: "dfp", bidTimeout: 2000 }, - bidders: [ - { + bidders: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { name: "ix", siteId: "300" - } - ] + }] } } } diff --git a/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html new file mode 100644 index 00000000000..75a72ba3501 --- /dev/null +++ b/integrationExamples/videoModule/jwplayer/bidsBackHandlerOverride.html @@ -0,0 +1,144 @@ + + + + + + + JW Player with Bids Back Handler override + + + + + +

JW Player with Bids Back Handler override

+
Div-1: Player placeholder div
+
+ + + diff --git a/integrationExamples/videoModule/jwplayer/eventListeners.html b/integrationExamples/videoModule/jwplayer/eventListeners.html index 5332270f560..6f04f37264b 100644 --- a/integrationExamples/videoModule/jwplayer/eventListeners.html +++ b/integrationExamples/videoModule/jwplayer/eventListeners.html @@ -23,6 +23,13 @@ // Replace this object to test a new Adapter! bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', @@ -37,7 +44,7 @@ divId: 'player', vendorCode: 1, // vendorCode for jwplayer playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', diff --git a/integrationExamples/videoModule/jwplayer/eventsUI.html b/integrationExamples/videoModule/jwplayer/eventsUI.html index bdfe5f3b883..cfd1efe7624 100644 --- a/integrationExamples/videoModule/jwplayer/eventsUI.html +++ b/integrationExamples/videoModule/jwplayer/eventsUI.html @@ -22,6 +22,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', @@ -39,7 +46,7 @@ divId: 'player', vendorCode: 1, // jwplayer vendorCode playerConfig: { - licenseKey: '577+5vXsluqV2Uy0drAS8wrgiqJlYijZxz3DmoYDm8FTJjdoIe8zYA==', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { playlist: [{ diff --git a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html index fc7c1b9486c..1f4331785ea 100644 --- a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html +++ b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html @@ -19,6 +19,13 @@ divId: 'player', // required to indicate which player is being used to render this ad unit. }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', @@ -36,7 +43,7 @@ divId: 'player', vendorCode: 1, // JW Player vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { file: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4', diff --git a/integrationExamples/videoModule/jwplayer/mediaMetadata.html b/integrationExamples/videoModule/jwplayer/mediaMetadata.html index 03f74f5bd0f..63e62aa4b82 100644 --- a/integrationExamples/videoModule/jwplayer/mediaMetadata.html +++ b/integrationExamples/videoModule/jwplayer/mediaMetadata.html @@ -20,6 +20,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', @@ -37,7 +44,7 @@ divId: 'player', vendorCode: 1, // JW Player vendorCode playerConfig: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', params: { vendorConfig: { mediaid: 'XYXYXYXY', diff --git a/integrationExamples/videoModule/jwplayer/playlist.html b/integrationExamples/videoModule/jwplayer/playlist.html index 223fee15c6f..9e89f606f23 100644 --- a/integrationExamples/videoModule/jwplayer/playlist.html +++ b/integrationExamples/videoModule/jwplayer/playlist.html @@ -20,6 +20,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', @@ -38,7 +45,7 @@ vendorCode: 1, // JW Player vendorCode playerConfig: { params: { - licenseKey: 'IAjLREYRLylTWsfLN3FoN/O3iQLbs+AfgZLlkAoyH8gSf7TnNtmOLcR8CUY=', + licenseKey: 'zwqnWJlovTKhXv2JIcKBj0Si//K7cVPmBDEyaILcAMw+nVKaizsJRA==', vendorConfig: { playlist: [{ mediaid: 'XYXYXYXY', diff --git a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html index d6656bc0c93..35745ab281f 100644 --- a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html @@ -35,6 +35,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/bidRequestScheduling.html b/integrationExamples/videoModule/videojs/bidRequestScheduling.html index eb10fda89a2..da6499ca4cc 100644 --- a/integrationExamples/videoModule/videojs/bidRequestScheduling.html +++ b/integrationExamples/videoModule/videojs/bidRequestScheduling.html @@ -37,6 +37,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html new file mode 100644 index 00000000000..74217ecee2c --- /dev/null +++ b/integrationExamples/videoModule/videojs/bidsBackHandlerOverride.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + VideoJS with Bids Back Handler override + + + + + +

VideoJS with Bids Back Handler override

+
Div-1: Player placeholder div
+ + + + + + diff --git a/integrationExamples/videoModule/videojs/eventListeners.html b/integrationExamples/videoModule/videojs/eventListeners.html index 1966f134e02..3fc2ef7137c 100644 --- a/integrationExamples/videoModule/videojs/eventListeners.html +++ b/integrationExamples/videoModule/videojs/eventListeners.html @@ -35,6 +35,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/eventsUI.html b/integrationExamples/videoModule/videojs/eventsUI.html index 9eba09f7a52..215b2de4d25 100644 --- a/integrationExamples/videoModule/videojs/eventsUI.html +++ b/integrationExamples/videoModule/videojs/eventsUI.html @@ -37,6 +37,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/gamAdServerMediation.html b/integrationExamples/videoModule/videojs/gamAdServerMediation.html index 6ffc1a67c03..d6603abbf8f 100644 --- a/integrationExamples/videoModule/videojs/gamAdServerMediation.html +++ b/integrationExamples/videoModule/videojs/gamAdServerMediation.html @@ -34,6 +34,13 @@ divId: 'player', // required to indicate which player is being used to render this ad unit. }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/mediaMetadata.html b/integrationExamples/videoModule/videojs/mediaMetadata.html index ede076fd814..084c597cddd 100644 --- a/integrationExamples/videoModule/videojs/mediaMetadata.html +++ b/integrationExamples/videoModule/videojs/mediaMetadata.html @@ -35,6 +35,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/integrationExamples/videoModule/videojs/playlist.html b/integrationExamples/videoModule/videojs/playlist.html index eb813f095f7..2563717df41 100644 --- a/integrationExamples/videoModule/videojs/playlist.html +++ b/integrationExamples/videoModule/videojs/playlist.html @@ -36,6 +36,13 @@ }, bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }, { bidder: 'ix', params: { siteId: '300', diff --git a/karma.conf.maker.js b/karma.conf.maker.js index e05d5b08afd..7b8ca13b20b 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -15,8 +15,7 @@ function newWebpackConfig(codeCoverage, disableFeatures) { mode: 'development', devtool: 'inline-source-map', }); - - delete webpackConfig.entry; + ['entry', 'optimization'].forEach(prop => delete webpackConfig[prop]); webpackConfig.module.rules .flatMap((r) => r.use) diff --git a/libraries/appnexusKeywords/anKeywords.js b/libraries/appnexusUtils/anKeywords.js similarity index 89% rename from libraries/appnexusKeywords/anKeywords.js rename to libraries/appnexusUtils/anKeywords.js index 5dc0b453253..a6fa8d7a21e 100644 --- a/libraries/appnexusKeywords/anKeywords.js +++ b/libraries/appnexusUtils/anKeywords.js @@ -1,4 +1,4 @@ -import {_each, deepAccess, getValueString, isArray, isStr, mergeDeep, isNumber} from '../../src/utils.js'; +import {_each, deepAccess, isArray, isNumber, isStr, mergeDeep, logWarn} from '../../src/utils.js'; import {getAllOrtbKeywords} from '../keywords/keywords.js'; import {CLIENT_SECTIONS} from '../../src/fpd/oneClient.js'; @@ -12,6 +12,19 @@ const ORTB_SEG_PATHS = ['user.data'].concat( CLIENT_SECTIONS.map((prefix) => `${prefix}.content.data`) ); +function getValueString(param, val, defaultValue) { + if (val === undefined || val === null) { + return defaultValue; + } + if (isStr(val)) { + return val; + } + if (isNumber(val)) { + return val.toString(); + } + logWarn('Unsuported type for param: ' + param + ' required type: String'); +} + /** * Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties * normally read from bidder params @@ -65,7 +78,7 @@ export function convertKeywordStringToANMap(keyStr) { } /** - * @param {Array} kwarray: keywords as an array of strings + * @param {Array} kwarray keywords as an array of strings * @return {{}} appnexus-style keyword map */ function convertKeywordsToANMap(kwarray) { diff --git a/libraries/appnexusUtils/anUtils.js b/libraries/appnexusUtils/anUtils.js new file mode 100644 index 00000000000..9b55cd5c2a4 --- /dev/null +++ b/libraries/appnexusUtils/anUtils.js @@ -0,0 +1,25 @@ +/** + * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' + * @param {string} value string value to convert + */ +import {deepClone, isPlainObject} from '../../src/utils.js'; + +export function convertCamelToUnderscore(value) { + return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { + return '_' + y.toLowerCase(); + }).replace(/^_/, ''); +} + +/** + * Creates an array of n length and fills each item with the given value + */ +export function fill(value, length) { + let newArray = []; + + for (let i = 0; i < length; i++) { + let valueToPush = isPlainObject(value) ? deepClone(value) : value; + newArray.push(valueToPush); + } + + return newArray; +} diff --git a/libraries/autoplayDetection/autoplay.js b/libraries/autoplayDetection/autoplay.js new file mode 100644 index 00000000000..b598e46cbd1 --- /dev/null +++ b/libraries/autoplayDetection/autoplay.js @@ -0,0 +1,42 @@ +let autoplayEnabled = null; + +/** + * Note: this function returns true if detection is not done yet. This is by design: if autoplay is not allowed, + * the call to video.play() will fail immediately, otherwise it may not terminate. + * @returns true if autoplay is not forbidden + */ +export const isAutoplayEnabled = () => autoplayEnabled !== false; + +// generated with: +// ask ChatGPT for a 160x90 black PNG image (1/8th the size of 720p) +// +// encode with: +// ffmpeg -i black_image_160x90.png -r 1 -c:v libx264 -bsf:v 'filter_units=remove_types=6' -pix_fmt yuv420p autoplay.mp4 +// this creates a 1 second long, 1 fps YUV 4:2:0 video encoded with H.264 without encoder details. +// +// followed by: +// echo "data:video/mp4;base64,$(base64 -i autoplay.mp4)" + +const autoplayVideoUrl = + 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAADxtZGF0AAAAMGWIhAAV//73ye/Apuvb3rW/k89I/Cy3PsIqP39atohOSV14BYa1heKCYgALQC5K4QAAAwZtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAD6AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACMHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAD6AAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAoAAAAFoAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAA+gAAAAAAAEAAAAAAahtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAAEAAAABAAFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAFTbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABE3N0YmwAAACvc3RzZAAAAAAAAAABAAAAn2F2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAoABaAEgAAABIAAAAAAAAAAEVTGF2YzYwLjMxLjEwMiBsaWJ4MjY0AAAAAAAAAAAAAAAY//8AAAA1YXZjQwFkAAr/4QAYZ2QACqzZQo35IQAAAwABAAADAAIPEiWWAQAGaOvjyyLA/fj4AAAAABRidHJ0AAAAAAAAAaAAAAGgAAAAGHN0dHMAAAAAAAAAAQAAAAEAAEAAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHN6AAAAAAAAADQAAAABAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjYwLjE2LjEwMA=='; + +function startDetection() { + // we create an HTMLVideoElement muted and not displayed in which we try to play a one frame video + const videoElement = document.createElement('video'); + videoElement.src = autoplayVideoUrl; + videoElement.setAttribute('playsinline', 'true'); + videoElement.muted = true; + + videoElement + .play() + .then(() => { + autoplayEnabled = true; + videoElement.pause(); + }) + .catch(() => { + autoplayEnabled = false; + }); +} + +// starts detection as soon as this library is loaded +startDetection(); diff --git a/libraries/chunk/chunk.js b/libraries/chunk/chunk.js new file mode 100644 index 00000000000..57be7bd5016 --- /dev/null +++ b/libraries/chunk/chunk.js @@ -0,0 +1,19 @@ +/** + * http://npm.im/chunk + * Returns an array with *size* chunks from given array + * + * Example: + * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => + * [['a', 'b'], ['c', 'd'], ['e']] + */ +export function chunk(array, size) { + let newArray = []; + + for (let i = 0; i < Math.ceil(array.length / size); i++) { + let start = i * size; + let end = start + size; + newArray.push(array.slice(start, end)); + } + + return newArray; +} diff --git a/libraries/cmp/cmpClient.js b/libraries/cmp/cmpClient.js index 0e2336cae7a..1d0b327cee4 100644 --- a/libraries/cmp/cmpClient.js +++ b/libraries/cmp/cmpClient.js @@ -4,19 +4,43 @@ import {GreedyPromise} from '../../src/utils/promise.js'; * @typedef {function} CMPClient * * @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}. - * @returns {Promise<*>} a promise that: - * - if a `callback` param was provided, resolves (with no result) just before the first time it's run; - * - if `callback` was *not* provided, resolves to the return value of the CMP command + * @param {boolean} once if true, discard cross-frame event listeners once a reply message is received. + * @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined. * @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required) + * @property {() => void} close close the client; currently, this just stops listening for cross-frame messages. */ +export const MODE_MIXED = 0; +export const MODE_RETURN = 1; +export const MODE_CALLBACK = 2; + /** - * Returns a function that can interface with a CMP regardless of where it's located. + * Returns a client function that can interface with a CMP regardless of where it's located. + * + * @param {object} obj + * @param obj.apiName name of the CMP api, e.g. "__gpp" + * @param [obj.apiVersion] CMP API version + * @param [obj.apiArgs] names of the arguments taken by the api function, in order. + * @param [obj.callbackArgs] names of the cross-frame response payload properties that should be passed as callback arguments, in order + * @param [obj.mode] controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved. + * + * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these + * cases "subscriptions" and "one-shot calls" respectively: + * + * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback + * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and + * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's + * return value when it's directly accessible, or with the result from the first (and, presumably, the only) + * cross-frame reply when it's not; + * + * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined + * when cross-frame; + * + * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead, + * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will + * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve + * the returned promise. Subscriptions are treated in the same way as MODE_MIXED. * - * @param apiName name of the CMP api, e.g. "__gpp" - * @param apiVersion? CMP API version - * @param apiArgs? names of the arguments taken by the api function, in order. - * @param callbackArgs? names of the cross-frame response payload properties that should be passed as callback arguments, in order * @param win * @returns {CMPClient} CMP invocation function (or null if no CMP was found). */ @@ -26,6 +50,7 @@ export function cmpClient( apiVersion, apiArgs = ['command', 'callback', 'parameter', 'version'], callbackArgs = ['returnValue', 'success'], + mode = MODE_MIXED, }, win = window ) { @@ -89,15 +114,15 @@ export function cmpClient( } function wrapCallback(callback, resolve, reject, preamble) { + const haveCb = typeof callback === 'function'; + return function (result, success) { preamble && preamble(); - const resolver = success == null || success ? resolve : reject; - if (typeof callback === 'function') { - resolver(); - return callback.apply(this, arguments); - } else { - resolver(result); + if (mode !== MODE_RETURN) { + const resolver = success == null || success ? resolve : reject; + resolver(haveCb ? undefined : result); } + haveCb && callback.apply(this, arguments); } } @@ -108,9 +133,9 @@ export function cmpClient( return new GreedyPromise((resolve, reject) => { const ret = cmpFrame[apiName](...resolveParams({ ...params, - callback: params.callback && wrapCallback(params.callback, resolve, reject) + callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined, }).map(([_, val]) => val)); - if (params.callback == null) { + if (mode === MODE_RETURN || (params.callback == null && mode === MODE_MIXED)) { resolve(ret); } }); @@ -118,7 +143,7 @@ export function cmpClient( } else { win.addEventListener('message', handleMessage, false); - client = function invokeCMPFrame(params) { + client = function invokeCMPFrame(params, once = false) { return new GreedyPromise((resolve, reject) => { // call CMP via postMessage const callId = Math.random().toString(); @@ -129,11 +154,16 @@ export function cmpClient( } }; - cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, params?.callback == null && (() => { delete cmpCallbacks[callId] })); + cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, (once || params?.callback == null) && (() => { delete cmpCallbacks[callId] })); cmpFrame.postMessage(msg, '*'); + if (mode === MODE_RETURN) resolve(); }); }; } - client.isDirect = isDirect; - return client; + return Object.assign(client, { + isDirect, + close() { + !isDirect && win.removeEventListener('message', handleMessage); + } + }) } diff --git a/libraries/creative-renderer-display/renderer.js b/libraries/creative-renderer-display/renderer.js new file mode 100644 index 00000000000..72f3658fe79 --- /dev/null +++ b/libraries/creative-renderer-display/renderer.js @@ -0,0 +1,2 @@ +// this file is autogenerated, see creative/README.md +export const RENDERER = "!function(){\"use strict\";window.render=function({ad:d,adUrl:i,width:n,height:e},{mkFrame:o},r){if(!d&&!i)throw{reason:\"noAd\",message:\"Missing ad markup or URL\"};{const t=r.document,s={width:n,height:e};i&&!d?s.src=i:s.srcdoc=d,t.body.appendChild(o(t,s))}}}();" \ No newline at end of file diff --git a/libraries/creative-renderer-native/renderer.js b/libraries/creative-renderer-native/renderer.js new file mode 100644 index 00000000000..509f7943af4 --- /dev/null +++ b/libraries/creative-renderer-native/renderer.js @@ -0,0 +1,2 @@ +// this file is autogenerated, see creative/README.md +export const RENDERER = "!function(){\"use strict\";const e=JSON.parse('{\"X3\":{\"B5\":\"Prebid Native\"}}').X3.B5,t={title:\"text\",data:\"value\",img:\"url\",video:\"vasttag\"};function n(e,t){return new Promise(((n,r)=>{const i=t.createElement(\"script\");i.onload=n,i.onerror=r,i.src=e,t.body.appendChild(i)}))}function r(e,t,r,i,o=n){const{rendererUrl:s,assets:a,ortb:d,adTemplate:c}=t,l=i.document;return s?o(s,l).then((()=>{if(\"function\"!=typeof i.renderAd)throw new Error(`Renderer from '${s}' does not define renderAd()`);const e=a||[];return e.ortb=d,i.renderAd(e)})):Promise.resolve(r(c??l.body.innerHTML))}window.render=function({adId:n,native:i},{sendMessage:o},s,a=r){const{head:d,body:c}=s.document,l=()=>o(e,{action:\"resizeNativeHeight\",height:c.offsetHeight,width:c.offsetWidth}),u=function(e,{assets:n=[],ortb:r,nativeKeys:i={}}){const o=Object.fromEntries(n.map((({key:e,value:t})=>[e,t])));let s=Object.fromEntries(Object.entries(i).flatMap((([t,n])=>{const r=o.hasOwnProperty(t)?o[t]:void 0;return[[`##${n}##`,r],[`${n}:${e}`,r]]})));return r&&Object.assign(s,{\"##hb_native_linkurl##\":r.link?.url,\"##hb_native_privacy##\":r.privacy},Object.fromEntries((r.assets||[]).flatMap((e=>{const n=Object.keys(t).find((t=>e[t]));return[n&&[`##hb_native_asset_id_${e.id}##`,e[n][t[n]]],e.link?.url&&[`##hb_native_asset_link_id_${e.id}##`,e.link.url]].filter((e=>e))})))),s=Object.entries(s).concat([[/##hb_native_asset_(link_)?id_\\d+##/g]]),function(e){return s.reduce(((e,[t,n])=>e.replaceAll(t,n||\"\")),e)}}(n,i);return d&&(d.innerHTML=u(d.innerHTML)),a(n,i,u,s).then((t=>{c.innerHTML=t,\"function\"==typeof s.postRenderAd&&s.postRenderAd({adId:n,...i}),s.document.querySelectorAll(\".pb-click\").forEach((t=>{const n=t.getAttribute(\"hb_native_asset_id\");t.addEventListener(\"click\",(()=>o(e,{action:\"click\",assetId:n})))})),o(e,{action:\"fireNativeImpressionTrackers\"}),\"complete\"===s.document.readyState?l():s.onload=l}))}}();" \ No newline at end of file diff --git a/libraries/currencyUtils/currency.js b/libraries/currencyUtils/currency.js new file mode 100644 index 00000000000..924f8f200d8 --- /dev/null +++ b/libraries/currencyUtils/currency.js @@ -0,0 +1,31 @@ +import {getGlobal} from '../../src/prebidGlobal.js'; +import {keyCompare} from '../../src/utils/reducers.js'; + +/** + * Attempt to convert `amount` from the currency `fromCur` to the currency `toCur`. + * + * By default, when the conversion is not possible (currency module not present or + * throwing errors), the amount is returned unchanged. This behavior can be + * toggled off with bestEffort = false. + */ +export function convertCurrency(amount, fromCur, toCur, bestEffort = true) { + if (fromCur === toCur) return amount; + let result = amount; + try { + result = getGlobal().convertCurrency(amount, fromCur, toCur); + } catch (e) { + if (!bestEffort) throw e; + } + return result; +} + +export function currencyNormalizer(toCurrency = null, bestEffort = true, convert = convertCurrency) { + return function (amount, currency) { + if (toCurrency == null) toCurrency = currency; + return convert(amount, currency, toCurrency, bestEffort); + } +} + +export function currencyCompare(get = (obj) => [obj.cpm, obj.currency], normalize = currencyNormalizer()) { + return keyCompare(obj => normalize.apply(null, get(obj))) +} diff --git a/libraries/gptUtils/gptUtils.js b/libraries/gptUtils/gptUtils.js new file mode 100644 index 00000000000..950f28c618f --- /dev/null +++ b/libraries/gptUtils/gptUtils.js @@ -0,0 +1,37 @@ +import {find} from '../../src/polyfill.js'; +import {compareCodeAndSlot, isGptPubadsDefined} from '../../src/utils.js'; + +/** + * Returns filter function to match adUnitCode in slot + * @param {string} adUnitCode AdUnit code + * @return {function} filter function + */ +export function isSlotMatchingAdUnitCode(adUnitCode) { + return (slot) => compareCodeAndSlot(slot, adUnitCode); +} + +/** + * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page + */ +export function getGptSlotForAdUnitCode(adUnitCode) { + let matchingSlot; + if (isGptPubadsDefined()) { + // find the first matching gpt slot on the page + matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); + } + return matchingSlot; +} + +/** + * @summary Uses the adUnit's code in order to find a matching gptSlot on the page + */ +export function getGptSlotInfoForAdUnitCode(adUnitCode) { + const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); + if (matchingSlot) { + return { + gptSlot: matchingSlot.getAdUnitPath(), + divId: matchingSlot.getSlotElementId() + }; + } + return {}; +} diff --git a/libraries/htmlEscape/htmlEscape.js b/libraries/htmlEscape/htmlEscape.js new file mode 100644 index 00000000000..f0952c02e3c --- /dev/null +++ b/libraries/htmlEscape/htmlEscape.js @@ -0,0 +1,26 @@ +/** + * Encode a string for inclusion in HTML. + * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and + * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ + * @return {string} + */ +export const escapeUnsafeChars = (() => { + const escapes = { + '<': '\\u003C', + '>': '\\u003E', + '/': '\\u002F', + '\\': '\\\\', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\0', + '\u2028': '\\u2028', + '\u2029': '\\u2029' + }; + + return function (str) { + return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]); + }; +})(); diff --git a/libraries/keywords/keywords.js b/libraries/keywords/keywords.js index 645c9c8d38f..b317bcf0c6b 100644 --- a/libraries/keywords/keywords.js +++ b/libraries/keywords/keywords.js @@ -6,7 +6,7 @@ const ORTB_KEYWORDS_PATHS = ['user.keywords'].concat( ); /** - * @param commaSeparatedKeywords: any number of either keyword arrays, or comma-separated keyword strings + * @param commaSeparatedKeywords any number of either keyword arrays, or comma-separated keyword strings * @returns an array with all unique keywords contained across all inputs */ export function mergeKeywords(...commaSeparatedKeywords) { diff --git a/libraries/mspa/activityControls.js b/libraries/mspa/activityControls.js index 3359862a5b3..eaf515e2385 100644 --- a/libraries/mspa/activityControls.js +++ b/libraries/mspa/activityControls.js @@ -6,9 +6,16 @@ import { ACTIVITY_TRANSMIT_PRECISE_GEO } from '../../src/activities/activities.js'; import {gppDataHandler} from '../../src/adapterManager.js'; +import {logInfo} from '../../src/utils.js'; // default interpretation for MSPA consent(s): -// https://docs.google.com/document/d/1yPEVx3bBdSkIw-QNkQR5qi1ztmn9zoXk-LaGQB6iw7Q +// https://docs.prebid.org/features/mspa-usnat.html + +const SENSITIVE_DATA_GEO = 7; + +function isApplicable(val) { + return val != null && val !== 0 +} export function isBasicConsentDenied(cd) { // service provider mode is always consent denied @@ -18,47 +25,62 @@ export function isBasicConsentDenied(cd) { // minors 13+ who have not given consent cd.KnownChildSensitiveDataConsents[0] === 1 || // minors under 13 cannot consent - cd.KnownChildSensitiveDataConsents[1] !== 0 || - // sensitive category consent impossible without notice - (cd.SensitiveDataProcessing.some(element => element === 2) && (cd.SensitiveDataLimitUseNotice !== 1 || cd.SensitiveDataProcessingOptOutNotice !== 1)); + isApplicable(cd.KnownChildSensitiveDataConsents[1]) || + // covered cannot be zero + cd.MspaCoveredTransaction === 0; } -export function isSensitiveNoticeMissing(cd) { - return ['SensitiveDataProcessingOptOutNotice', 'SensitiveDataLimitUseNotice'].some(prop => cd[prop] === 2) +export function sensitiveNoticeIs(cd, value) { + return ['SensitiveDataProcessingOptOutNotice', 'SensitiveDataLimitUseNotice'].some(prop => cd[prop] === value) } export function isConsentDenied(cd) { return isBasicConsentDenied(cd) || - // user opts out - (['SaleOptOut', 'SharingOptOut', 'TargetedAdvertisingOptOut'].some(prop => cd[prop] === 1)) || - // notice not given - (['SaleOptOutNotice', 'SharingNotice', 'SharingOptOutNotice', 'TargetedAdvertisingOptOutNotice'].some(prop => cd[prop] === 2)) || - // sale opted in but notice does not apply - ((cd.SaleOptOut === 2 && cd.SaleOptOutNotice === 0)) || - // targeted opted in but notice does not apply - ((cd.TargetedAdvertisingOptOut === 2 && cd.TargetedAdvertisingOptOutNotice === 0)) || - // sharing opted in but notices do not apply - ((cd.SharingOptOut === 2 && (cd.SharingNotice === 0 || cd.SharingOptOutNotice === 0))); + ['Sale', 'Sharing', 'TargetedAdvertising'].some(scope => { + const oo = cd[`${scope}OptOut`]; + const notice = cd[`${scope}OptOutNotice`]; + // user opted out + return oo === 1 || + // opt-out notice was not given + notice === 2 || + // do not trust CMP if it signals opt-in but no opt-out notice was given + (oo === 2 && notice === 0); + }) || + // no sharing notice was given ... + cd.SharingNotice === 2 || + // ... or the CMP says it was not applicable, while also claiming it got consent + (cd.SharingOptOut === 2 && cd.SharingNotice === 0); } -export function isTransmitUfpdConsentDenied(cd) { - // SensitiveDataProcessing[1-5,11]=1 OR SensitiveDataProcessing[6,7,9,10,12]<>0 OR - const mustBeZero = [6, 7, 9, 10, 12]; - const mustBeZeroSubtractedVector = mustBeZero.map((number) => number - 1); - const SensitiveDataProcessingMustBeZero = cd.SensitiveDataProcessing.filter(index => mustBeZeroSubtractedVector.includes(index)); - const cannotBeOne = [1, 2, 3, 4, 5, 11]; - const cannotBeOneSubtractedVector = cannotBeOne.map((number) => number - 1); - const SensitiveDataProcessingCannotBeOne = cd.SensitiveDataProcessing.filter(index => cannotBeOneSubtractedVector.includes(index)); - return isConsentDenied(cd) || - isSensitiveNoticeMissing(cd) || - SensitiveDataProcessingCannotBeOne.some(val => val === 1) || - SensitiveDataProcessingMustBeZero.some(val => val !== 0); -} +export const isTransmitUfpdConsentDenied = (() => { + // deny anything that smells like: genetic, biometric, state/national ID, financial, union membership, + // or personal communication data + const cannotBeInScope = [6, 7, 9, 10, 12].map(el => --el); + // require consent for everything else (except geo, which is treated separately) + const allExceptGeo = Array.from(Array(12).keys()).filter((el) => el !== SENSITIVE_DATA_GEO) + const mustHaveConsent = allExceptGeo.filter(el => !cannotBeInScope.includes(el)); + + return function (cd) { + return isConsentDenied(cd) || + // no notice about sensitive data was given + sensitiveNoticeIs(cd, 2) || + // extra-sensitive data is applicable + cannotBeInScope.some(i => isApplicable(cd.SensitiveDataProcessing[i])) || + // user opted out for not-as-sensitive data + mustHaveConsent.some(i => cd.SensitiveDataProcessing[i] === 1) || + // CMP says it has consent, but did not give notice about it + (sensitiveNoticeIs(cd, 0) && allExceptGeo.some(i => cd.SensitiveDataProcessing[i] === 2)) + } +})(); export function isTransmitGeoConsentDenied(cd) { - return isBasicConsentDenied(cd) || - isSensitiveNoticeMissing(cd) || - cd.SensitiveDataProcessing[7] === 1 + const geoConsent = cd.SensitiveDataProcessing[SENSITIVE_DATA_GEO]; + return geoConsent === 1 || + isBasicConsentDenied(cd) || + // no sensitive data notice was given + sensitiveNoticeIs(cd, 2) || + // do not trust CMP if it says it has consent for geo but didn't show a sensitive data notice + (sensitiveNoticeIs(cd, 0) && geoConsent === 2) } const CONSENT_RULES = { @@ -66,26 +88,40 @@ const CONSENT_RULES = { [ACTIVITY_ENRICH_EIDS]: isConsentDenied, [ACTIVITY_ENRICH_UFPD]: isTransmitUfpdConsentDenied, [ACTIVITY_TRANSMIT_PRECISE_GEO]: isTransmitGeoConsentDenied -} +}; export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDataHandler.getConsentData()?.applicableSections) { - return function() { + return function () { if (applicableSids().some(sid => sids.includes(sid))) { const consent = getConsent(); if (consent == null) { return {allow: false, reason: 'consent data not available'}; } if (denies(consent)) { - return {allow: false} + return {allow: false}; } } - } + }; +} + +function flatSection(subsections) { + if (subsections == null) return subsections; + return subsections.reduceRight((subsection, consent) => { + return Object.assign(consent, subsection); + }, {}); } export function setupRules(api, sids, normalizeConsent = (c) => c, rules = CONSENT_RULES, registerRule = registerActivityControl, getConsentData = () => gppDataHandler.getConsentData()) { const unreg = []; + const ruleName = `MSPA (GPP '${api}' for section${sids.length > 1 ? 's' : ''} ${sids.join(', ')})`; + logInfo(`Enabling activity controls for ${ruleName}`) Object.entries(rules).forEach(([activity, denies]) => { - unreg.push(registerRule(activity, `MSPA (${api})`, mspaRule(sids, () => normalizeConsent(getConsentData()?.sectionData?.[api]), denies, () => getConsentData()?.applicableSections || []))) - }) - return () => unreg.forEach(ur => ur()) + unreg.push(registerRule(activity, ruleName, mspaRule( + sids, + () => normalizeConsent(flatSection(getConsentData()?.parsedSections?.[api])), + denies, + () => getConsentData()?.applicableSections || [] + ))); + }); + return () => unreg.forEach(ur => ur()); } diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js index cf3d2f38256..784c3f1444d 100644 --- a/libraries/objectGuard/objectGuard.js +++ b/libraries/objectGuard/objectGuard.js @@ -2,6 +2,8 @@ import {isData, objectTransformer, sessionedApplies} from '../../src/activities/ import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js'; /** + * @typedef {import('../src/activities/redactor.js').TransformationRuleDef} TransformationRuleDef + * @typedef {import('../src/adapters/bidderFactory.js').TransformationRule} TransformationRule * @typedef {Object} ObjectGuard * @property {*} obj a view on the guarded object * @property {function(): void} verify a function that checks for and rolls back disallowed changes to the guarded object diff --git a/libraries/objectGuard/ortbGuard.js b/libraries/objectGuard/ortbGuard.js index 7911b378c3d..62918d55548 100644 --- a/libraries/objectGuard/ortbGuard.js +++ b/libraries/objectGuard/ortbGuard.js @@ -9,6 +9,10 @@ import { import {objectGuard, writeProtectRule} from './objectGuard.js'; import {mergeDeep} from '../../src/utils.js'; +/** + * @typedef {import('./objectGuard.js').ObjectGuard} ObjectGuard + */ + function ortb2EnrichRules(isAllowed = isActivityAllowed) { return [ { diff --git a/libraries/ortbConverter/README.md b/libraries/ortbConverter/README.md index 31f56b4c754..751971eebdc 100644 --- a/libraries/ortbConverter/README.md +++ b/libraries/ortbConverter/README.md @@ -80,8 +80,7 @@ However, there are two restrictions (to avoid them, use the [other customization ) ``` - -### Fine grained customization - imp, request, bidResponse, response +### Fine grained customization - imp, request, bidResponse, response When invoked, `toORTB({bidRequests, bidderRequest})` first loops through each request in `bidRequests`, converting them into ORTB `imp` objects. It then packages them into a single ORTB request, adding other parameters that are not imp-specific (such as for example `request.tmax`). @@ -91,7 +90,7 @@ a single return value. You can customize each of these steps using the `ortbConverter` arguments `imp`, `request`, `bidResponse` and `response`: -### Customizing imps: `imp(buildImp, bidRequest, context)` +### Customizing imps: `imp(buildImp, bidRequest, context)` Invoked once for each input `bidRequest`; should return the ORTB `imp` object to include in the request. The arguments are: @@ -101,7 +100,7 @@ The arguments are: - `context`: a [context object](#context) that contains at least: - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`. -#### Example: attaching custom bid params +#### Example: attaching custom bid params ```javascript const converter = ortbConverter({ @@ -351,7 +350,7 @@ const converter = ortbConverter({ - the `context` argument of `ortbConverter`: e.g. `ortbConverter({context: {ttl: 30}})`. This will set `context.ttl = 30` globally for the converter. - the `context` argument of `toORTB`: e.g. `converter.toORTB({bidRequests, bidderRequest, context: {ttl: 30}})`. This will set `context.ttl = 30` only for this request. -### Special `context` properties +### Special `context` properties For ease of use, the conversion logic gives special meaning to some context properties: diff --git a/libraries/ortbConverter/processors/default.js b/libraries/ortbConverter/processors/default.js index 8db2c1c461e..d92a51daba2 100644 --- a/libraries/ortbConverter/processors/default.js +++ b/libraries/ortbConverter/processors/default.js @@ -97,6 +97,9 @@ export const DEFAULT_PROCESSORS = { if (bid.adomain) { bidResponse.meta.advertiserDomains = bid.adomain; } + if (bid.ext?.dsa) { + bidResponse.meta.dsa = bid.ext.dsa; + } } } } diff --git a/libraries/percentInView/percentInView.js b/libraries/percentInView/percentInView.js new file mode 100644 index 00000000000..13381c5c541 --- /dev/null +++ b/libraries/percentInView/percentInView.js @@ -0,0 +1,63 @@ + +function getBoundingBox(element, {w, h} = {}) { + let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); + + if ((width === 0 || height === 0) && w && h) { + width = w; + height = h; + right = left + w; + bottom = top + h; + } + + return {width, height, left, top, right, bottom}; +} + +function getIntersectionOfRects(rects) { + const bbox = { + left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom + }; + + for (let i = 1; i < rects.length; ++i) { + bbox.left = Math.max(bbox.left, rects[i].left); + bbox.right = Math.min(bbox.right, rects[i].right); + + if (bbox.left >= bbox.right) { + return null; + } + + bbox.top = Math.max(bbox.top, rects[i].top); + bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); + + if (bbox.top >= bbox.bottom) { + return null; + } + } + + bbox.width = bbox.right - bbox.left; + bbox.height = bbox.bottom - bbox.top; + + return bbox; +} + +export const percentInView = (element, topWin, {w, h} = {}) => { + const elementBoundingBox = getBoundingBox(element, {w, h}); + + // Obtain the intersection of the element and the viewport + const elementInViewBoundingBox = getIntersectionOfRects([{ + left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight + }, elementBoundingBox]); + + let elementInViewArea, elementTotalArea; + + if (elementInViewBoundingBox !== null) { + // Some or all of the element is in view + elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; + elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; + + return ((elementInViewArea / elementTotalArea) * 100); + } + + // No overlap between element and the viewport; therefore, the element + // lies completely out of view + return 0; +} diff --git a/libraries/sizeUtils/sizeUtils.js b/libraries/sizeUtils/sizeUtils.js new file mode 100644 index 00000000000..41cdd71df89 --- /dev/null +++ b/libraries/sizeUtils/sizeUtils.js @@ -0,0 +1,29 @@ +/** + * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) + * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` + * @param {object} adUnit one adUnit object from the normal list of adUnits + * @returns {Array.} array of arrays containing numeric sizes + */ +export function getAdUnitSizes(adUnit) { + if (!adUnit) { + return; + } + + let sizes = []; + if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { + let bannerSizes = adUnit.mediaTypes.banner.sizes; + if (Array.isArray(bannerSizes[0])) { + sizes = bannerSizes; + } else { + sizes.push(bannerSizes); + } + // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders + } else if (Array.isArray(adUnit.sizes)) { + if (Array.isArray(adUnit.sizes[0])) { + sizes = adUnit.sizes; + } else { + sizes.push(adUnit.sizes); + } + } + return sizes; +} diff --git a/libraries/transformParamsUtils/convertTypes.js b/libraries/transformParamsUtils/convertTypes.js new file mode 100644 index 00000000000..813d8e6e693 --- /dev/null +++ b/libraries/transformParamsUtils/convertTypes.js @@ -0,0 +1,36 @@ +import {isFn} from '../../src/utils.js'; + +/** + * Try to convert a value to a type. + * If it can't be done, the value will be returned. + * + * @param {string} typeToConvert The target type. e.g. "string", "number", etc. + * @param {*} value The value to be converted into typeToConvert. + */ +function tryConvertType(typeToConvert, value) { + if (typeToConvert === 'string') { + return value && value.toString(); + } else if (typeToConvert === 'number') { + return Number(value); + } else { + return value; + } +} + +export function convertTypes(types, params) { + Object.keys(types).forEach(key => { + if (params[key]) { + if (isFn(types[key])) { + params[key] = types[key](params[key]); + } else { + params[key] = tryConvertType(types[key], params[key]); + } + + // don't send invalid values + if (isNaN(params[key])) { + delete params.key; + } + } + }); + return params; +} diff --git a/libraries/uid1Eids/uid1Eids.js b/libraries/uid1Eids/uid1Eids.js new file mode 100644 index 00000000000..5bf3dde5c6c --- /dev/null +++ b/libraries/uid1Eids/uid1Eids.js @@ -0,0 +1,16 @@ +export const UID1_EIDS = { + 'tdid': { + source: 'adserver.org', + atype: 1, + getValue: function(data) { + if (data.id) { + return data.id; + } else { + return data; + } + }, + getUidExt: function(data) { + return {...{rtiPartner: 'TDID'}, ...data.ext} + } + } +} diff --git a/libraries/uid2Eids/uid2Eids.js b/libraries/uid2Eids/uid2Eids.js new file mode 100644 index 00000000000..ce4f4fa3b2a --- /dev/null +++ b/libraries/uid2Eids/uid2Eids.js @@ -0,0 +1,14 @@ +export const UID2_EIDS = { + 'uid2': { + source: 'uidapi.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } +} diff --git a/libraries/urlUtils/urlUtils.js b/libraries/urlUtils/urlUtils.js new file mode 100644 index 00000000000..f0c5823aab1 --- /dev/null +++ b/libraries/urlUtils/urlUtils.js @@ -0,0 +1,7 @@ +export function tryAppendQueryString(existingUrl, key, value) { + if (value) { + return existingUrl + key + '=' + encodeURIComponent(value) + '&'; + } + + return existingUrl; +} diff --git a/libraries/vastTrackers/vastTrackers.js b/libraries/vastTrackers/vastTrackers.js new file mode 100644 index 00000000000..b4ae98aba57 --- /dev/null +++ b/libraries/vastTrackers/vastTrackers.js @@ -0,0 +1,95 @@ +import {addBidResponse} from '../../src/auction.js'; +import {VIDEO} from '../../src/mediaTypes.js'; +import {logError} from '../../src/utils.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_REPORT_ANALYTICS} from '../../src/activities/activities.js'; +import {activityParams} from '../../src/activities/activityParams.js'; + +const vastTrackers = []; + +addBidResponse.before(function (next, adUnitcode, bidResponse, reject) { + if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { + const vastTrackers = getVastTrackers(bidResponse); + if (vastTrackers) { + bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml); + const impTrackers = vastTrackers.get('impressions'); + if (impTrackers) { + bidResponse.vastImpUrl = [].concat(impTrackers).concat(bidResponse.vastImpUrl).filter(t => t); + } + } + } + next(adUnitcode, bidResponse, reject); +}); + +export function registerVastTrackers(moduleType, moduleName, trackerFn) { + if (typeof trackerFn === 'function') { + vastTrackers.push({'moduleType': moduleType, 'moduleName': moduleName, 'trackerFn': trackerFn}); + } +} + +export function insertVastTrackers(trackers, vastXml) { + const doc = new DOMParser().parseFromString(vastXml, 'text/xml'); + const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine'); + try { + if (wrappers.length) { + wrappers.forEach(wrapper => { + if (trackers.get('impressions')) { + trackers.get('impressions').forEach(trackingUrl => { + const impression = doc.createElement('Impression'); + impression.appendChild(doc.createCDATASection(trackingUrl)); + wrapper.appendChild(impression); + }); + } + }); + vastXml = new XMLSerializer().serializeToString(doc); + } + } catch (error) { + logError('an error happened trying to insert trackers in vastXml'); + } + return vastXml; +} + +export function getVastTrackers(bid) { + let trackers = []; + vastTrackers.filter( + ({ + moduleType, + moduleName, + trackerFn + }) => isActivityAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(moduleType, moduleName)) + ).forEach(({trackerFn}) => { + let trackersToAdd = trackerFn(bid); + trackersToAdd.forEach(trackerToAdd => { + if (isValidVastTracker(trackers, trackerToAdd)) { + trackers.push(trackerToAdd); + } + }); + }); + const trackersMap = trackersToMap(trackers); + return (trackersMap.size ? trackersMap : null); +}; + +function isValidVastTracker(trackers, trackerToAdd) { + return trackerToAdd.hasOwnProperty('event') && trackerToAdd.hasOwnProperty('url'); +} + +function trackersToMap(trackers) { + return trackers.reduce((map, {url, event}) => { + !map.has(event) && map.set(event, new Set()); + map.get(event).add(url); + return map; + }, new Map()); +} + +export function addImpUrlToTrackers(bid, trackersMap) { + if (bid.vastImpUrl) { + if (!trackersMap) { + trackersMap = new Map(); + } + if (!trackersMap.get('impressions')) { + trackersMap.set('impressions', new Set()); + } + trackersMap.get('impressions').add(bid.vastImpUrl); + } + return trackersMap; +} diff --git a/libraries/video/shared/parentModule.js b/libraries/video/shared/parentModule.js index 06c71ebd75b..b040f39bcb8 100644 --- a/libraries/video/shared/parentModule.js +++ b/libraries/video/shared/parentModule.js @@ -47,6 +47,7 @@ export function ParentModule(submoduleBuilder_) { } /** + * @typedef {import('../../../modules/videoModule/coreVideo.js').vendorSubmoduleDirectory} vendorSubmoduleDirectory * @typedef {Object} SubmoduleBuilder * @summary Instantiates submodules * @param {vendorSubmoduleDirectory} submoduleDirectory_ diff --git a/modules/.submodules.json b/modules/.submodules.json index b45fb7f2303..cfa98b5ab32 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -46,7 +46,9 @@ "zeotapIdPlusIdSystem", "adqueryIdSystem", "gravitoIdSystem", - "freepassIdSystem" + "freepassIdSystem", + "operaadsIdSystem", + "mygaruIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -54,30 +56,45 @@ ], "rtdModule": [ "1plusXRtdProvider", + "a1MediaRtdProvider", "aaxBlockmeterRtdProvider", + "adlooxRtdProvider", + "adnuntiusRtdProvider", "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", + "azerionedgeRtdProvider", "blueconicRtdProvider", + "brandmetricsRtdProvider", "browsiRtdProvider", "captifyRtdProvider", + "mediafilterRtdProvider", "confiantRtdProvider", "dgkeywordRtdProvider", + "experianRtdProvider", "geoedgeRtdProvider", + "geolocationRtdProvider", + "greenbidsRtdProvider", + "growthCodeRtdProvider", "hadronRtdProvider", - "haloRtdProvider", "iasRtdProvider", + "idWardRtdProvider", + "imRtdProvider", + "intersectionRtdProvider", "jwplayerRtdProvider", "medianetRtdProvider", "mgidRtdProvider", + "neuwoRtdProvider", "oneKeyRtdProvider", "optimeraRtdProvider", + "oxxionRtdProvider", "permutiveRtdProvider", + "qortexRtdProvider", "reconciliationRtdProvider", + "relevadRtdProvider", "sirdataRtdProvider", "timeoutRtdProvider", - "weboramaRtdProvider", - "zeusPrimeRtdProvider" + "weboramaRtdProvider" ], "fpdModule": [ "validationFpdModule", @@ -86,6 +103,9 @@ "videoModule": [ "jwplayerVideoProvider", "videojsVideoProvider" + ], + "paapi": [ + "fledgeForGpt" ] } } diff --git a/modules/33acrossAnalyticsAdapter.js b/modules/33acrossAnalyticsAdapter.js new file mode 100644 index 00000000000..e3539906b13 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.js @@ -0,0 +1,656 @@ +import { deepAccess, logInfo, logWarn, logError, deepClone } from '../src/utils.js'; +import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager, { coppaDataHandler, gdprDataHandler, gppDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; + +/** + * @typedef {typeof import('../src/constants.json').EVENTS} EVENTS + */ +const { EVENTS } = CONSTANTS; + +/** @typedef {'pending'|'available'|'targetingSet'|'rendered'|'timeout'|'rejected'|'noBid'|'error'} BidStatus */ +/** + * @type {Object} + */ +const BidStatus = { + PENDING: 'pending', + AVAILABLE: 'available', + TARGETING_SET: 'targetingSet', + RENDERED: 'rendered', + TIMEOUT: 'timeout', + REJECTED: 'rejected', + NOBID: 'noBid', + ERROR: 'error', +} + +const ANALYTICS_VERSION = '1.0.0'; +const PROVIDER_NAME = '33across'; +const GVLID = 58; +/** Time to wait for all transactions in an auction to complete before sending the report */ +const DEFAULT_TRANSACTION_TIMEOUT = 10000; +/** Time to wait after all GAM slots have registered before sending the report */ +export const POST_GAM_TIMEOUT = 500; +export const DEFAULT_ENDPOINT = 'https://analytics.33across.com/api/v1/event'; + +export const log = getLogger(); + +/** + * @typedef {Object} AnalyticsReport - Sent when all bids are complete (as determined by `bidWon` and `slotRenderEnded` events) + * @property {string} analyticsVersion - Version of the Prebid.js 33Across Analytics Adapter + * @property {string} pid - Partner ID + * @property {string} src - Source of the report ('pbjs') + * @property {string} pbjsVersion - Version of Prebid.js + * @property {Auction[]} auctions + */ + +/** + * @typedef {Object} AnalyticsCache + * @property {string} pid Partner ID + * @property {Object} auctions + * @property {string} [usPrivacy] + */ + +/** + * @typedef {Object} Auction - Parsed auction data + * @property {AdUnit[]} adUnits + * @property {string} auctionId + * @property {string[]} userIds + */ + +/** + * @typedef {`${number}x${number}`} AdUnitSize + */ + +/** + * @typedef {('banner'|'native'|'video')} AdUnitMediaType + */ + +/** + * @typedef {Object} BidResponse + * @property {number} cpm + * @property {string} cur + * @property {number} [cpmOrig] + * @property {number} cpmFloor + * @property {AdUnitMediaType} mediaType + * @property {AdUnitSize} size + */ + +/** + * @typedef {Object} Bid - Parsed bid data + * @property {string} bidder + * @property {string} bidId + * @property {string} source + * @property {string} status + * @property {BidResponse} [bidResponse] + * @property {1|0} [hasWon] + */ + +/** + * @typedef {Object} AdUnit - Parsed adUnit data + * @property {string} transactionId - Primary key for *this* auction/adUnit combination + * @property {string} adUnitCode + * @property {string} slotId - Equivalent to GPID. (Note that + * GPID supports adUnits where multiple units have the same `code` values + * by appending a `#UNIQUIFIER`. The value of the UNIQUIFIER is likely to be the div-id, + * but, if div-id is randomized / unavailable, may be something else like the media size) + * @property {Array} mediaTypes + * @property {Array} sizes + * @property {Array} bids + */ + +/** + * After the first transaction begins, wait until all transactions are complete + * before calling `onComplete`. If the timeout is reached before all transactions + * are complete, send the report anyway. + * + * Use this to track all transactions per auction, and send the report as soon + * as all adUnits have been won (or after timeout) even if other bid/auction + * activity is still happening. + */ +class TransactionManager { + /** + * Milliseconds between activity to allow until this collection automatically completes. + * @type {number} + */ + #sendTimeout; + #sendTimeoutId; + #transactionsPending = new Set(); + #transactionsCompleted = new Set(); + #onComplete; + + constructor({ timeout, onComplete }) { + this.#sendTimeout = timeout; + this.#onComplete = onComplete; + } + + status() { + return { + pending: [...this.#transactionsPending], + completed: [...this.#transactionsCompleted], + }; + } + + initiate(transactionId) { + this.#transactionsPending.add(transactionId); + this.#restartSendTimeout(); + } + + complete(transactionId) { + if (!this.#transactionsPending.has(transactionId)) { + log.warn(`transactionId "${transactionId}" was not found. No transaction to mark as complete.`); + return; + } + + this.#transactionsPending.delete(transactionId); + this.#transactionsCompleted.add(transactionId); + + if (this.#transactionsPending.size === 0) { + this.#flushTransactions(); + } + } + + #flushTransactions() { + this.#clearSendTimeout(); + this.#transactionsPending = new Set(); + this.#onComplete(); + } + + // gulp-eslint is using eslint 6, a version that doesn't support private method syntax + // eslint-disable-next-line no-dupe-class-members + #clearSendTimeout() { + return clearTimeout(this.#sendTimeoutId); + } + + // eslint-disable-next-line no-dupe-class-members + #restartSendTimeout() { + this.#clearSendTimeout(); + + this.#sendTimeoutId = setTimeout(() => { + if (this.#sendTimeout !== 0) { + log.warn(`Timed out waiting for ad transactions to complete. Sending report.`); + } + + this.#flushTransactions(); + }, this.#sendTimeout); + } +} + +/** + * Initialized during `enableAnalytics`. Exported for testing purposes. + */ +export const locals = { + /** @type {Object} - one manager per auction */ + transactionManagers: {}, + /** @type {AnalyticsCache} */ + cache: { + auctions: {}, + pid: '', + }, + /** @type {Object} */ + adUnitMap: {}, + reset() { + this.transactionManagers = {}; + this.cache = { + auctions: {}, + pid: '', + }; + this.adUnitMap = {}; + } +} + +/** + * @typedef {Object} AnalyticsAdapter + * @property {function} track + * @property {function} enableAnalytics + * @property {function} disableAnalytics + * @property {function} [originEnableAnalytics] + * @property {function} [originDisableAnalytics] + * @property {function} [_oldEnable] + */ + +/** + * @type {AnalyticsAdapter} + */ +const analyticsAdapter = Object.assign( + buildAdapter({ analyticsType: 'endpoint' }), + { track: analyticEventHandler } +); + +analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics; +analyticsAdapter.enableAnalytics = enableAnalyticsWrapper; + +/** + * @typedef {Object} AnalyticsConfig + * @property {string} provider - set by pbjs at module registration time + * @property {Object} options + * @property {string} options.pid - Publisher/Partner ID + * @property {string} [options.endpoint=DEFAULT_ENDPOINT] - Endpoint to send analytics data + * @property {number} [options.timeout=DEFAULT_TRANSACTION_TIMEOUT] - Timeout for sending analytics data + */ + +/** + * @param {AnalyticsConfig} config Analytics module configuration + */ +function enableAnalyticsWrapper(config) { + const { options } = config; + + const pid = options.pid; + if (!pid) { + log.error('No partnerId provided for "options.pid". No analytics will be sent.'); + + return; + } + + const endpoint = calculateEndpoint(options.endpoint); + this.getUrl = () => endpoint; + + const timeout = calculateTransactionTimeout(options.timeout); + this.getTimeout = () => timeout; + + locals.cache = { + pid, + auctions: {}, + }; + + window.googletag = window.googletag || { cmd: [] }; + window.googletag.cmd.push(subscribeToGamSlots); + + analyticsAdapter.originEnableAnalytics(config); +} + +/** + * @param {string} [endpoint] + * @returns {string} + */ +function calculateEndpoint(endpoint = DEFAULT_ENDPOINT) { + if (typeof endpoint === 'string' && endpoint.startsWith('http')) { + return endpoint; + } + + log.info(`Invalid endpoint provided for "options.endpoint". Using default endpoint.`); + + return DEFAULT_ENDPOINT; +} +/** + * @param {number} [configTimeout] + * @returns {number} Transaction Timeout + */ +function calculateTransactionTimeout(configTimeout = DEFAULT_TRANSACTION_TIMEOUT) { + if (typeof configTimeout === 'number' && configTimeout >= 0) { + return configTimeout; + } + + log.info(`Invalid timeout provided for "options.timeout". Using default timeout of ${DEFAULT_TRANSACTION_TIMEOUT}ms.`); + + return DEFAULT_TRANSACTION_TIMEOUT; +} + +function subscribeToGamSlots() { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + setTimeout(() => { + const { transactionId, auctionId } = + getAdUnitMetadata(event.slot.getAdUnitPath(), event.slot.getSlotElementId()); + if (!transactionId || !auctionId) { + const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; + log.warn('Could not find configured ad unit matching GAM render of slot:', { slotName }); + return; + } + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); + }, POST_GAM_TIMEOUT); + }); +} + +function getAdUnitMetadata(adUnitPath, adSlotElementId) { + const adUnitMeta = locals.adUnitMap[adUnitPath] || locals.adUnitMap[adSlotElementId]; + if (adUnitMeta && adUnitMeta.length > 0) { + return adUnitMeta[adUnitMeta.length - 1]; + } + return {}; +} + +/** necessary for testing */ +analyticsAdapter.originDisableAnalytics = analyticsAdapter.disableAnalytics; +analyticsAdapter.disableAnalytics = function () { + analyticsAdapter._oldEnable = enableAnalyticsWrapper; + locals.reset(); + analyticsAdapter.originDisableAnalytics(); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: analyticsAdapter, + code: PROVIDER_NAME, + gvlid: GVLID, +}); + +export default analyticsAdapter; + +/** + * @param {AnalyticsCache} analyticsCache + * @param {string} completedAuctionId value of auctionId + * @return {AnalyticsReport} Analytics report + */ +function createReportFromCache(analyticsCache, completedAuctionId) { + const { pid, auctions } = analyticsCache; + + const report = { + pid, + src: 'pbjs', + analyticsVersion: ANALYTICS_VERSION, + pbjsVersion: '$prebid.version$', // Replaced by build script + auctions: [ auctions[completedAuctionId] ], + } + if (uspDataHandler.getConsentData()) { + report.usPrivacy = uspDataHandler.getConsentData(); + } + + if (gdprDataHandler.getConsentData()) { + report.gdpr = Number(Boolean(gdprDataHandler.getConsentData().gdprApplies)); + report.gdprConsent = gdprDataHandler.getConsentData().consentString || ''; + } + + if (gppDataHandler.getConsentData()) { + report.gpp = gppDataHandler.getConsentData().gppString; + report.gppSid = gppDataHandler.getConsentData().applicableSections; + } + + if (coppaDataHandler.getCoppa()) { + report.coppa = Number(coppaDataHandler.getCoppa()); + } + + return report; +} + +function getCachedBid(auctionId, bidId) { + const auction = locals.cache.auctions[auctionId]; + for (let adUnit of auction.adUnits) { + for (let bid of adUnit.bids) { + if (bid.bidId === bidId) { + return bid; + } + } + } + log.error(`Cannot find bid "${bidId}" in auction "${auctionId}".`); +}; + +/** + * @param {Object} args + * @param {Object} args.args Event data + * @param {EVENTS[keyof EVENTS]} args.eventType + */ +function analyticEventHandler({ eventType, args }) { + if (!locals.cache) { + log.error('Something went wrong. Analytics cache is not initialized.'); + return; + } + + switch (eventType) { + case EVENTS.AUCTION_INIT: + onAuctionInit(args); + break; + case EVENTS.BID_REQUESTED: // BidStatus.PENDING + onBidRequested(args); + break; + case EVENTS.BID_TIMEOUT: + for (let bid of args) { + setCachedBidStatus(bid.auctionId, bid.bidId, BidStatus.TIMEOUT); + } + break; + case EVENTS.BID_RESPONSE: + onBidResponse(args); + break; + case EVENTS.BID_REJECTED: + onBidRejected(args); + break; + case EVENTS.NO_BID: + case EVENTS.SEAT_NON_BID: + setCachedBidStatus(args.auctionId, args.bidId, BidStatus.NOBID); + break; + case EVENTS.BIDDER_ERROR: + if (args.bidderRequest && args.bidderRequest.bids) { + for (let bid of args.bidderRequest.bids) { + setCachedBidStatus(args.bidderRequest.auctionId, bid.bidId, BidStatus.ERROR); + } + } + break; + case EVENTS.AUCTION_END: + onAuctionEnd(args); + break; + case EVENTS.BID_WON: // BidStatus.TARGETING_SET | BidStatus.RENDERED | BidStatus.ERROR + onBidWon(args); + break; + default: + break; + } +} + +/**************** + * AUCTION_INIT * + ***************/ +function onAuctionInit({ adUnits, auctionId, bidderRequests }) { + if (typeof auctionId !== 'string' || !Array.isArray(bidderRequests)) { + log.error('Analytics adapter failed to parse auction.'); + return; + } + + locals.cache.auctions[auctionId] = { + auctionId, + adUnits: adUnits.map(au => { + setAdUnitMap(au.code, auctionId, au.transactionId); + + return { + transactionId: au.transactionId, + adUnitCode: au.code, + // Note: GPID supports adUnits that have matching `code` values by appending a `#UNIQUIFIER`. + // The value of the UNIQUIFIER is likely to be the div-id, + // but, if div-id is randomized / unavailable, may be something else like the media size) + slotId: deepAccess(au, 'ortb2Imp.ext.gpid') || deepAccess(au, 'ortb2Imp.ext.data.pbadslot', au.code), + mediaTypes: Object.keys(au.mediaTypes), + sizes: au.sizes.map(size => size.join('x')), + bids: [], + } + }), + userIds: Object.keys(deepAccess(bidderRequests, '0.bids.0.userId', {})), + }; + + locals.transactionManagers[auctionId] ||= + new TransactionManager({ + timeout: analyticsAdapter.getTimeout(), + onComplete() { + sendReport( + createReportFromCache(locals.cache, auctionId), + analyticsAdapter.getUrl() + ); + delete locals.transactionManagers[auctionId]; + } + }); +} + +function setAdUnitMap(adUnitCode, auctionId, transactionId) { + if (!locals.adUnitMap[adUnitCode]) { + locals.adUnitMap[adUnitCode] = []; + } + + locals.adUnitMap[adUnitCode].push({ auctionId, transactionId }); +} + +/***************** + * BID_REQUESTED * + ****************/ +function onBidRequested({ auctionId, bids }) { + for (let { bidder, bidId, transactionId, src } of bids) { + const auction = locals.cache.auctions[auctionId]; + const adUnit = auction.adUnits.find(adUnit => adUnit.transactionId === transactionId); + if (!adUnit) return; + adUnit.bids.push({ + bidder, + bidId, + status: BidStatus.PENDING, + hasWon: 0, + source: src, + }); + + // if there is no manager for this auction, then the auction has already been completed + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].initiate(transactionId); + } +} + +/**************** + * BID_RESPONSE * + ***************/ +function onBidResponse({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, size, status, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, status); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size + }, + source + } + ); +} + +/**************** + * BID_REJECTED * + ***************/ +function onBidRejected({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, width, height, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, BidStatus.REJECTED); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size: `${width}x${height}` + }, + source + } + ); +} + +/*************** + * AUCTION_END * + **************/ +/** + * @param {Object} args + * @param {{requestId: string, status: string}[]} args.bidsReceived + * @param {string} args.auctionId + * @returns {void} + */ +function onAuctionEnd({ bidsReceived, auctionId }) { + for (let bid of bidsReceived) { + setCachedBidStatus(auctionId, bid.requestId, bid.status); + } +} + +/*********** + * BID_WON * + **********/ +function onBidWon(bidWon) { + const { auctionId, requestId, transactionId } = bidWon; + const bid = getCachedBid(auctionId, requestId); + if (!bid) { + return; + } + + setBidStatus(bid, bidWon.status ?? BidStatus.ERROR); + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); +} + +/** + * @param {Bid} bid + * @param {BidStatus} [status] + * @returns {void} + */ +function setBidStatus(bid, status = BidStatus.AVAILABLE) { + const statusStates = { + pending: { + next: [BidStatus.AVAILABLE, BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + available: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + targetingSet: { + next: [BidStatus.RENDERED, BidStatus.ERROR, BidStatus.TIMEOUT], + }, + rendered: { + next: [], + }, + timeout: { + next: [], + }, + rejected: { + next: [], + }, + noBid: { + next: [], + }, + error: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + } + + const winningStatuses = [BidStatus.RENDERED]; + + if (statusStates[bid.status].next.includes(status)) { + bid.status = status; + if (winningStatuses.includes(status)) { + // occassionally we can detect a bidWon before prebid reports it as such + bid.hasWon = 1; + } + } +} + +function setCachedBidStatus(auctionId, bidId, status) { + const bid = getCachedBid(auctionId, bidId); + if (!bid) return; + setBidStatus(bid, status); +} + +/** + * Guarantees sending of data without waiting for response, even after page is left/closed + * + * @param {AnalyticsReport} report Request payload + * @param {string} endpoint URL + */ +function sendReport(report, endpoint) { + if (navigator.sendBeacon(endpoint, JSON.stringify(report))) { + log.info(`Analytics report sent to ${endpoint}`, report); + + return; + } + + log.error('Analytics report exceeded User-Agent data limits and was not sent.', report); +} + +/** + * Encapsulate certain logger functions and add a prefix to the final messages. + * + * @return {Object} New logger functions + */ +function getLogger() { + const LPREFIX = `${PROVIDER_NAME} Analytics: `; + + return { + info: (msg, ...args) => logInfo(`${LPREFIX}${msg}`, ...deepClone(args)), + warn: (msg, ...args) => logWarn(`${LPREFIX}${msg}`, ...deepClone(args)), + error: (msg, ...args) => logError(`${LPREFIX}${msg}`, ...deepClone(args)), + } +} diff --git a/modules/33acrossAnalyticsAdapter.md b/modules/33acrossAnalyticsAdapter.md new file mode 100644 index 00000000000..c56059e5526 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.md @@ -0,0 +1,76 @@ +# Overview + +```txt +Module Name: 33Across Analytics Adapter +Module Type: Analytics Adapter +Maintainer: analytics_support@33across.com +``` + +#### About + +This analytics adapter collects data about the performance of your ad slots +for each auction run on your site. It also provides insight into how identifiers +from the +[33Across User ID Sub-module](https://docs.prebid.org/dev-docs/modules/userid-submodules/33across.html) +and other user ID sub-modules improve your monetization. The data is sent at +the earliest opportunity for each auction to provide a more complete picture of +your ad performance. + +The analytics adapter is free to use! +However, the publisher must work with our account management team to obtain a +Publisher/Partner ID (PID) and enable Analytics for their account. +To get a PID and to have the publisher account enabled for Analytics, +you can reach out to our team at the following email - analytics_support@33across.com + +If you are an existing publisher and you already use a 33Across PID, +you can reach out to analytics_support@33across.com +to have your account enabled for analytics. + +The 33Across privacy policy is at . + +#### Analytics Options + +| Name | Scope | Example | Type | Description | +|-----------|----------|---------|----------|-------------| +| `pid` | required | abc123 | `string` | 33Across Publisher ID | +| `timeout` | optional | 10000 | `int` | Milliseconds to wait after last seen auction transaction before sending report (default 10000). | + +#### Configuration + +The data is sent at the earliest opportunity for each auction to provide +a more complete picture of your ad performance, even if the auction is interrupted +by a page navigation. At the latest, the adapter will always send the report +when the page is unloaded, at the end of the auction, or after the timeout, +whichever comes first. + +In order to guarantee consistent reports of your ad slot behavior, we recommend +including the GPT Pre-Auction Module, `gptPreAuction`. This module is included +by default when Prebid is downloaded. If you are compiling from source, +this might look something like: + +```sh +gulp bundle --modules=gptPreAuction,consentManagement,consentManagementGpp,consentManagementUsp,enrichmentFpdModule,gdprEnforcement,33acrossBidAdapter,33acrossIdSystem,33acrossAnalyticsAdapter +``` + +Enable the 33Across Analytics Adapter in Prebid.js using the analytics provider `33across` +and options as seen in the example below. + +#### Example Configuration + +```js +pbjs.enableAnalytics({ + provider: '33across', + options: { + /** + * The 33Across Publisher ID. + */ + pid: 'abc123', + /** + * Timeout in milliseconds after which an auction report + * will be sent regardless of auction state. + * [optional] + */ + timeout: 10000 + } +}); +``` diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index b965183de19..0e9beb22013 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -6,7 +6,6 @@ import { getWindowTop, isArray, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logWarn, mergeDeep, @@ -14,6 +13,7 @@ import { uniques } from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; // **************************** UTILS *************************** // const BIDDER_CODE = '33across'; diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 0f370237e21..33086562111 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -5,44 +5,59 @@ * @requires module:modules/userId */ -import { logMessage, logError } from '../src/utils.js'; +import { logMessage, logError, logWarn } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; +import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = '33acrossId'; const API_URL = 'https://lexicon.33across.com/v1/envelope'; const AJAX_TIMEOUT = 10000; const CALLER_NAME = 'pbjs'; +const GVLID = 58; + +const STORAGE_FPID_KEY = '33acrossIdFp'; -function getEnvelope(response) { +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +function calculateResponseObj(response) { if (!response.succeeded) { if (response.error == 'Cookied User') { logMessage(`${MODULE_NAME}: Unsuccessful response`.concat(' ', response.error)); } else { logError(`${MODULE_NAME}: Unsuccessful response`.concat(' ', response.error)); } - return; + return {}; } if (!response.data.envelope) { logMessage(`${MODULE_NAME}: No envelope was received`); - return; + return {}; } - return response.data.envelope; + return { + envelope: response.data.envelope, + fp: response.data.fp + }; } -function calculateQueryStringParams(pid, gdprConsentData) { +function calculateQueryStringParams(pid, gdprConsentData, storageConfig) { const uspString = uspDataHandler.getConsentData(); - const gdprApplies = Boolean(gdprConsentData?.gdprApplies); const coppaValue = coppaDataHandler.getCoppa(); const gppConsent = gppDataHandler.getConsentData(); const params = { pid, - gdpr: Number(gdprApplies), + gdpr: 0, src: CALLER_NAME, ver: '$prebid.version$', coppa: Number(coppaValue) @@ -63,9 +78,49 @@ function calculateQueryStringParams(pid, gdprConsentData) { params.gdpr_consent = gdprConsentData.consentString; } + const fp = getStoredValue(STORAGE_FPID_KEY, storageConfig); + if (fp) { + params.fp = fp; + } + return params; } +function deleteFromStorage(key) { + if (storage.cookiesAreEnabled()) { + const expiredDate = new Date(0).toUTCString(); + + storage.setCookie(key, '', expiredDate, 'Lax'); + } + + storage.removeDataFromLocalStorage(key); +} + +function storeValue(key, value, storageConfig = {}) { + if (storageConfig.type === STORAGE_TYPE_COOKIES && storage.cookiesAreEnabled()) { + const expirationInMs = 60 * 60 * 24 * 1000 * storageConfig.expires; + const expirationTime = new Date(Date.now() + expirationInMs); + + storage.setCookie(key, value, expirationTime.toUTCString(), 'Lax'); + } else if (storageConfig.type === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } +} + +function getStoredValue(key, storageConfig = {}) { + if (storageConfig.type === STORAGE_TYPE_COOKIES && storage.cookiesAreEnabled()) { + return storage.getCookie(key); + } else if (storageConfig.type === STORAGE_TYPE_LOCALSTORAGE) { + return storage.getDataFromLocalStorage(key); + } +} + +function handleFpId(fpId, storageConfig = {}) { + fpId + ? storeValue(STORAGE_FPID_KEY, fpId, storageConfig) + : deleteFromStorage(STORAGE_FPID_KEY); +} + /** @type {Submodule} */ export const thirthyThreeAcrossIdSubmodule = { /** @@ -74,7 +129,7 @@ export const thirthyThreeAcrossIdSubmodule = { */ name: MODULE_NAME, - gvlid: 58, + gvlid: GVLID, /** * decode the stored id value for passing to bid requests @@ -96,34 +151,49 @@ export const thirthyThreeAcrossIdSubmodule = { * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId({ params = { } }, gdprConsentData) { + getId({ params = { }, storage: storageConfig }, gdprConsentData) { if (typeof params.pid !== 'string') { logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); return; } - const { pid, apiUrl = API_URL } = params; + if (gdprConsentData?.gdprApplies === true) { + logWarn(`${MODULE_NAME}: Submodule cannot be used where GDPR applies`); + + return; + } + + const { pid, storeFpid, apiUrl = API_URL } = params; return { callback(cb) { ajaxBuilder(AJAX_TIMEOUT)(apiUrl, { success(response) { - let envelope; + let responseObj = { }; try { - envelope = getEnvelope(JSON.parse(response)) + responseObj = calculateResponseObj(JSON.parse(response)); } catch (err) { logError(`${MODULE_NAME}: ID reading error:`, err); } - cb(envelope); + + if (!responseObj.envelope) { + deleteFromStorage(MODULE_NAME); + } + + if (storeFpid) { + handleFpId(responseObj.fp, storageConfig); + } + + cb(responseObj.envelope); }, error(err) { logError(`${MODULE_NAME}: ID error response`, err); cb(); } - }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); + }, calculateQueryStringParams(pid, gdprConsentData, storageConfig), { method: 'GET', withCredentials: true }); } }; }, diff --git a/modules/33acrossIdSystem.md b/modules/33acrossIdSystem.md index 1e4af89344f..8b73a43069d 100644 --- a/modules/33acrossIdSystem.md +++ b/modules/33acrossIdSystem.md @@ -15,7 +15,7 @@ pbjs.setConfig({ storage: { name: "33acrossId", type: "html5", - expires: 90, + expires: 30, refreshInSeconds: 8*3600 }, params: { @@ -41,7 +41,7 @@ The following settings are available for the `storage` property in the `userSync | --- | --- | --- | --- | --- | | name | Required | String| Name of the cookie or HTML5 local storage where the user ID will be stored | `"33acrossId"` | | type | Required | String | `"html5"` (preferred) or `"cookie"` | `"html5"` | -| expires | Strongly Recommended | Number | How long (in days) the user ID information will be stored. 33Across recommends `90`. | `90` | +| expires | Strongly Recommended | Number | How long (in days) the user ID information will be stored. 33Across recommends `30`. | `30` | | refreshInSeconds | Strongly Recommended | Number | The interval (in seconds) for refreshing the user ID. 33Across recommends no more than 8 hours between refreshes. | `8*3600` | ### Params @@ -51,3 +51,4 @@ The following settings are available in the `params` property in `userSync.userI | Param name | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | pid | Required | String | Partner ID provided by 33Across | `"0010b00002GYU4eBAH"` | +| storeFpid | Optional | Boolean | Indicates whether a supplemental first-party ID may be stored to improve addressability | `false` (default) or `true` | diff --git a/modules/BTBidAdapter.js b/modules/BTBidAdapter.js new file mode 100644 index 00000000000..7b50b90124b --- /dev/null +++ b/modules/BTBidAdapter.js @@ -0,0 +1,204 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, isPlainObject, logWarn } from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; + +const BIDDER_CODE = 'blockthrough'; +const GVLID = 815; +const ENDPOINT_URL = 'https://pbs.btloader.com/openrtb2/auction'; +const SYNC_URL = 'https://cdn.btloader.com/user_sync.html'; + +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: 60, + }, + imp, + request, + bidResponse, +}); + +/** + * Builds an impression object for the ORTB 2.5 request. + * + * @param {function} buildImp - The function for building an imp object. + * @param {Object} bidRequest - The bid request object. + * @param {Object} context - The context object. + * @returns {Object} The ORTB 2.5 imp object. + */ +function imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const { params, ortb2Imp } = bidRequest; + + if (params) { + deepSetValue(imp, 'ext', params); + } + if (ortb2Imp?.ext?.gpid) { + deepSetValue(imp, 'ext.gpid', ortb2Imp.ext.gpid); + } + + return imp; +} + +/** + * Builds a request object for the ORTB 2.5 request. + * + * @param {function} buildRequest - The function for building a request object. + * @param {Array} imps - An array of ORTB 2.5 impression objects. + * @param {Object} bidderRequest - The bidder request object. + * @param {Object} context - The context object. + * @returns {Object} The ORTB 2.5 request object. + */ +function request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'ext.prebid.channel', { + name: 'pbjs', + version: '$prebid.version$', + }); + + if (window.location.href.includes('btServerTest=true')) { + request.test = 1; + } + + return request; +} + +/** + * Processes a bid response using the provided build function, bid, and context. + * + * @param {Function} buildBidResponse - The function to build the bid response. + * @param {Object} bid - The bid object to include in the bid response. + * @param {Object} context - The context object containing additional information. + * @returns {Object} - The processed bid response. + */ +function bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + const { seat } = context.seatbid || {}; + bidResponse.btBidderCode = seat; + + return bidResponse; +} + +/** + * Checks if a bid request is valid. + * + * @param {Object} bid - The bid request object. + * @returns {boolean} True if the bid request is valid, false otherwise. + */ +function isBidRequestValid(bid) { + if (!isPlainObject(bid.params) || !Object.keys(bid.params).length) { + logWarn('BT Bid Adapter: bid params must be provided.'); + return false; + } + + return true; +} + +/** + * Builds the bid requests for the BT Service. + * + * @param {Array} validBidRequests - An array of valid bid request objects. + * @param {Object} bidderRequest - The bidder request object. + * @returns {Array} An array of BT Service bid requests. + */ +function buildRequests(validBidRequests, bidderRequest) { + const data = CONVERTER.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + }); + + return [ + { + method: 'POST', + url: ENDPOINT_URL, + data, + bids: validBidRequests, + }, + ]; +} + +/** + * Interprets the server response and maps it to bids. + * + * @param {Object} serverResponse - The server response object. + * @param {Object} request - The request object. + * @returns {Array} An array of bid objects. + */ +function interpretResponse(serverResponse, request) { + if (!serverResponse || !request) { + return []; + } + + return CONVERTER.fromORTB({ + response: serverResponse.body, + request: request.data, + }).bids; +} + +/** + * Generates user synchronization data based on provided options and consents. + * + * @param {Object} syncOptions - Synchronization options. + * @param {Object[]} serverResponses - An array of server responses. + * @param {Object} gdprConsent - GDPR consent information. + * @param {string} uspConsent - US Privacy consent string. + * @param {Object} gppConsent - Google Publisher Policies (GPP) consent information. + * @returns {Object[]} An array of user synchronization objects. + */ +function getUserSyncs( + syncOptions, + serverResponses, + gdprConsent, + uspConsent, + gppConsent +) { + if (!syncOptions.iframeEnabled || !serverResponses?.length) { + return []; + } + + const bidderCodes = new Set(); + serverResponses.forEach((serverResponse) => { + if (serverResponse?.body?.ext?.responsetimemillis) { + Object.keys(serverResponse.body.ext.responsetimemillis).forEach( + bidderCodes.add, + bidderCodes + ); + } + }); + + if (!bidderCodes.size) { + return []; + } + + const syncs = []; + const syncUrl = new URL(SYNC_URL); + syncUrl.searchParams.set('bidders', [...bidderCodes].join(',')); + + if (gdprConsent) { + syncUrl.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)); + syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); + } + if (gppConsent) { + syncUrl.searchParams.set('gpp', gppConsent.gppString); + syncUrl.searchParams.set('gpp_sid', gppConsent.applicableSections); + } + if (uspConsent) { + syncUrl.searchParams.set('us_privacy', uspConsent); + } + + syncs.push({ type: 'iframe', url: syncUrl.href }); + + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/BTBidAdapter.md b/modules/BTBidAdapter.md new file mode 100644 index 00000000000..e29bc688b0c --- /dev/null +++ b/modules/BTBidAdapter.md @@ -0,0 +1,70 @@ +# Overview + +**Module Name**: BT Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: engsupport@blockthrough.com + +# Description + +The BT Bidder Adapter provides an interface to the BT Service. The BT Bidder Adapter sends one request to the BT Service per ad unit. Behind the scenes, the BT Service further disperses requests to various configured exchanges. This operational model closely resembles that of Prebid Server, where a single request is made from the client side, and responses are gathered from multiple exchanges. + +The BT adapter requires setup and approval from the Blockthrough team. Please reach out to marketing@blockthrough.com for more information. + +# Bid Params + +| Key | Scope | Type | Description | +| ------ | -------- | ------ | -------------------------------------------------------------- | +| bidder | Required | Object | Bidder configuration. Could configure several bidders this way | + +# Bidder Config + +Make sure to set required ab, orgID, websiteID values received after approval using `pbjs.setBidderConfig`. + +## Example + +```javascript +pbjs.setBidderConfig({ + bidders: ['blockthrough'], + config: { + ortb2: { + site: { + ext: { + blockthrough: { + ab: false, + orgID: '4829301576428910', + websiteID: '5654012389765432', + }, + }, + }, + }, + }, +}); +``` + +## AdUnits configuration example + +```javascript +var adUnits = [ + { + code: 'banner-div-1', + mediaTypes: { + banner: { + sizes: [[728, 90]], + }, + }, + bids: [ + { + bidder: 'blockthrough', + params: { + bidderA: { + publisherId: 55555, + }, + bidderB: { + zoneId: 12, + }, + }, + }, + ], + }, +]; +``` diff --git a/modules/a1MediaBidAdapter.js b/modules/a1MediaBidAdapter.js new file mode 100644 index 00000000000..d640bbfe2d7 --- /dev/null +++ b/modules/a1MediaBidAdapter.js @@ -0,0 +1,104 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { replaceAuctionPrice } from '../src/utils.js'; + +const BIDDER_CODE = 'a1media'; +const END_POINT = 'https://d11.contentsfeed.com/dsp/breq/a1'; +const DEFAULT_CURRENCY = 'JPY'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + if (bidRequest.params.battr) { + Object.keys(bidRequest.mediaTypes).forEach(mType => { + imp[mType].battr = bidRequest.params.battr; + }) + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + if (!request.cur) { + request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + } + if (bid.params.bcat) { + request.bcat = bid.params.bcat; + } + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + + let resMediaType; + const reqMediaTypes = Object.keys(bidRequest.mediaTypes); + if (reqMediaTypes.length === 1) { + resMediaType = reqMediaTypes[0]; + } else { + if (bid.adm.search(/^(<\?xml| { + const parsedBid = seatbidItem.bid.map((bidItem) => ({ + ...bidItem, + adm: replaceAuctionPrice(bidItem.adm, bidItem.price), + nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price) + })); + return {...seatbidItem, bid: parsedBid}; + }); + + const responseBody = {...serverResponse.body, seatbid: parsedSeatbid}; + const bids = converter.fromORTB({ + response: responseBody, + request: bidRequest.data, + }).bids; + return bids; + }, + +}; +registerBidder(spec); diff --git a/modules/a1MediaBidAdapter.md b/modules/a1MediaBidAdapter.md new file mode 100644 index 00000000000..304b7e1bb5a --- /dev/null +++ b/modules/a1MediaBidAdapter.md @@ -0,0 +1,93 @@ +# Overview + +```markdown +Module Name: A1Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@a1mediagroup.co.kr +``` + +# Description + +Connects to A1Media exchange for bids. + +# Test Parameters + +## Sample Banner Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[320, 100]], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Video Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + mimes: ['video/mp4'], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Native Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + native: { + title: { + len: 140 + }, + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` diff --git a/modules/a1MediaRtdProvider.js b/modules/a1MediaRtdProvider.js new file mode 100644 index 00000000000..445ed47181d --- /dev/null +++ b/modules/a1MediaRtdProvider.js @@ -0,0 +1,96 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { isEmptyStr, mergeDeep } from '../src/utils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = 'a1Media'; +const SCRIPT_URL = 'https://linkback.contentsfeed.com/src'; +export const A1_SEG_KEY = '__a1tg'; +export const A1_AUD_KEY = 'a1_gid'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init: init, + getBidRequestData: alterBidRequests, +}; + +export function getStorageData(key) { + let storageValue = ''; + if (storage.getDataFromLocalStorage(key)) { + storageValue = storage.getDataFromLocalStorage(key); + } else if (storage.getCookie(key)) { + storageValue = storage.getCookie(key); + } + return storageValue; +} + +function loadLbScript(tagname) { + const linkback = window.linkback = window.linkback || {}; + if (!linkback.l) { + linkback.l = true; + + const scriptUrl = `${SCRIPT_URL}/${tagname}`; + loadExternalScript(scriptUrl, MODULE_NAME); + } +} + +function init(config, userConsent) { + const tagId = config.params.tagId; + if (tagId && !isEmptyStr(tagId)) { + loadLbScript(config.params.tagId); + return true; + } + if (!isEmptyStr(getStorageData(A1_SEG_KEY))) { + return true; + } + return false; +} + +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + const a1seg = getStorageData(A1_SEG_KEY); + const a1gid = getStorageData(A1_AUD_KEY); + + const a1UserSegData = { + name: 'a1mediagroup.com', + ext: { + segtax: 900 + }, + segment: a1seg.split(',').map(x => ({id: x})) + }; + + const a1UserEid = { + source: 'a1mediagroup.com', + uids: [ + { + id: a1gid, + atype: 1 + } + ] + }; + + const a1Ortb2 = { + user: { + data: [ + a1UserSegData + ], + ext: { + eids: [ + a1UserEid + ] + } + } + }; + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, a1Ortb2); + callback(); +} + +submodule(REAL_TIME_MODULE, subModuleObj); diff --git a/modules/a1MediaRtdProvider.md b/modules/a1MediaRtdProvider.md new file mode 100644 index 00000000000..ab2077ebbbb --- /dev/null +++ b/modules/a1MediaRtdProvider.md @@ -0,0 +1,46 @@ +# Overview + +Module Name: A1Media Rtd Provider +Module Type: Rtd Provider +Maintainer: dev@a1mediagroup.co.kr + +# Description + +This module loads external code using the passed parameter (params.tagId). + +The A1Media RTD module loads A1Media script for obtains user segments, and provides user segment data to bid-requests.
+to get user segments, you will need a1media script customized for site. + +To use this module, you’ll need to work with [A1MediaGroup](https://www.a1mediagroup.com/) to get an account and receive instructions on how to set up your pages and ad server. + +Contact dev@a1mediagroup.co.kr for information. + +### Integration + +1) Build the A1Media RTD Module into the Prebid.js package with: + +``` +gulp build --modules=a1MediaRtdProvider,... +``` + +2) Use `setConfig` to instruct Prebid.js to initilaize the A1Media RTD module, as specified below. + +### Configuration + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "a1Media", + waitForIt: true, + params: { + // 'tagId' is unique value for each account. + tagId: 'lb4test' + } + } + ] + } +}); +``` diff --git a/modules/ablidaBidAdapter.js b/modules/ablidaBidAdapter.js index 805a2020fb4..175d5ff7c72 100644 --- a/modules/ablidaBidAdapter.js +++ b/modules/ablidaBidAdapter.js @@ -3,6 +3,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'ablida'; const ENDPOINT_URL = 'https://bidder.ablida.net/prebid'; diff --git a/modules/acuityAdsBidAdapter.js b/modules/acuityadsBidAdapter.js similarity index 94% rename from modules/acuityAdsBidAdapter.js rename to modules/acuityadsBidAdapter.js index b0bb132ddae..5b12eb2133b 100644 --- a/modules/acuityAdsBidAdapter.js +++ b/modules/acuityadsBidAdapter.js @@ -153,6 +153,15 @@ export const spec = { tmax: bidderRequest.timeout }; + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; diff --git a/modules/acuityAdsBidAdapter.md b/modules/acuityadsBidAdapter.md similarity index 98% rename from modules/acuityAdsBidAdapter.md rename to modules/acuityadsBidAdapter.md index a19e0a6b0ba..7f001cd9376 100644 --- a/modules/acuityAdsBidAdapter.md +++ b/modules/acuityadsBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: AcuityAds Bidder Adapter Module Type: AcuityAds Bidder Adapter -Maintainer: sa-support@brightcom.com +Maintainer: rafi.babler@acuityads.com ``` # Description diff --git a/modules/ad2ictionBidAdapter.js b/modules/ad2ictionBidAdapter.js new file mode 100644 index 00000000000..0f7cea45d14 --- /dev/null +++ b/modules/ad2ictionBidAdapter.js @@ -0,0 +1,59 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const BIDDER_CODE = 'ad2iction'; +export const SUPPORTED_AD_TYPES = [BANNER]; +export const API_ENDPOINT = 'https://ads.ad2iction.com/html/prebid/'; +export const API_VERSION_NUMBER = 3; +export const COOKIE_NAME = 'ad2udid'; + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + aliases: ['ad2'], + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: (bid) => { + return !!bid.params.id && typeof bid.params.id === 'string'; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const ids = validBidRequests.map((bid) => { + return { bannerId: bid.params.id, bidId: bid.bidId }; + }); + + const options = { + contentType: 'application/json', + withCredentials: false, + }; + + const udid = storage.cookiesAreEnabled() && storage.getCookie(COOKIE_NAME); + + const data = { + ids: JSON.stringify(ids), + ortb2: bidderRequest.ortb2, + refererInfo: bidderRequest.refererInfo, + v: API_VERSION_NUMBER, + udid: udid || '', + _: Math.round(new Date().getTime()), + }; + + return { + method: 'POST', + url: API_ENDPOINT, + data, + options, + }; + }, + interpretResponse: (serverResponse, bidRequest) => { + if (!Array.isArray(serverResponse.body)) { + return []; + } + + const bidResponses = serverResponse.body; + + return bidResponses; + }, +}; + +registerBidder(spec); diff --git a/modules/ad2ictionBidAdapter.md b/modules/ad2ictionBidAdapter.md new file mode 100644 index 00000000000..47e355aa795 --- /dev/null +++ b/modules/ad2ictionBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +**Module Name**: Ad2iction Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@ad2iction.com + +# Description + +The Ad2iction Bidding adapter requires setup before beginning. Please contact us on https://www.ad2iction.com. + +# Sample Ad Unit Config +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]] + } + }, + bids: [{ + bidder: 'ad2iction', + params: { + id: 'accepted-uuid' + } + }] + } +]; +``` diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js index 36935e80d3b..d268c4cafa8 100644 --- a/modules/adWMGBidAdapter.js +++ b/modules/adWMGBidAdapter.js @@ -1,9 +1,9 @@ 'use strict'; -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'adWMG'; const ENDPOINT = 'https://hb.adwmg.com/hb'; diff --git a/modules/adagioAnalyticsAdapter.js b/modules/adagioAnalyticsAdapter.js index 96f3f089cbd..82b3b356c81 100644 --- a/modules/adagioAnalyticsAdapter.js +++ b/modules/adagioAnalyticsAdapter.js @@ -5,16 +5,52 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { getWindowTop } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getWindowTop, getWindowSelf, deepAccess, logInfo, logError } from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; const events = Object.keys(CONSTANTS.EVENTS).map(key => CONSTANTS.EVENTS[key]); -const VERSION = '2.0.0'; +const ADAGIO_GVLID = 617; +const VERSION = '3.0.0'; +const PREBID_VERSION = '$prebid.version$'; +const ENDPOINT = 'https://c.4dex.io/pba.gif'; +const CURRENCY_USD = 'USD'; +const ADAGIO_CODE = 'adagio'; +const cache = { + auctions: {}, + getAuction: function(auctionId, adUnitCode) { + return this.auctions[auctionId][adUnitCode]; + }, + getBiddersFromAuction: function(auctionId, adUnitCode) { + return this.getAuction(auctionId, adUnitCode).bdrs.split(','); + }, + getAllAdUnitCodes: function(auctionId) { + return Object.keys(this.auctions[auctionId]); + }, + updateAuction: function(auctionId, adUnitCode, values) { + this.auctions[auctionId][adUnitCode] = { + ...this.auctions[auctionId][adUnitCode], + ...values + }; + }, -const adagioEnqueue = function adagioEnqueue(action, data) { - getWindowTop().ADAGIO.queue.push({ action, data, ts: Date.now() }); -} + // Map prebid auction id to adagio auction id + auctionIdReferences: {}, + addPrebidAuctionIdRef(auctionId, adagioAuctionId) { + this.auctionIdReferences[auctionId] = adagioAuctionId; + }, + getAdagioAuctionId(auctionId) { + return this.auctionIdReferences[auctionId]; + } +}; +const enc = window.encodeURIComponent; + +/** +/* BEGIN ADAGIO.JS CODE + */ function canAccessTopWindow() { try { @@ -24,12 +60,357 @@ function canAccessTopWindow() { } catch (error) { return false; } +}; + +function getCurrentWindow() { + return currentWindow; +}; + +let currentWindow; + +const adagioEnqueue = function adagioEnqueue(action, data) { + getCurrentWindow().ADAGIO.queue.push({ action, data, ts: Date.now() }); +}; + +/** + * END ADAGIO.JS CODE + */ + +/** + * UTILS FUNCTIONS + */ + +const guard = { + adagio: (value) => isAdagio(value), + bidTracked: (auctionId, adUnitCode) => deepAccess(cache, `auctions.${auctionId}.${adUnitCode}`, false), + auctionTracked: (auctionId) => deepAccess(cache, `auctions.${auctionId}`, false) +}; + +function removeDuplicates(arr, getKey) { + const seen = {}; + return arr.filter(item => { + const key = getKey(item); + return seen.hasOwnProperty(key) ? false : (seen[key] = true); + }); +}; + +function isAdagio(alias) { + if (!alias) { + return false + } + return (alias + adapterManager.aliasRegistry[alias]).toLowerCase().includes(ADAGIO_CODE); +}; + +function getMediaTypeAlias(mediaType) { + const mediaTypesMap = { + banner: 'ban', + outstream: 'vidout', + instream: 'vidin', + adpod: 'vidadpod', + native: 'nat' + }; + return mediaTypesMap[mediaType] || mediaType; +}; + +function addKeyPrefix(obj, prefix) { + return Object.keys(obj).reduce((acc, key) => { + // We don't want to prefix already prefixed keys. + if (key.startsWith(prefix)) { + acc[key] = obj[key]; + return acc; + } + + acc[`${prefix}${key}`] = obj[key]; + return acc; + }, {}); +} + +function getUsdCpm(cpm, currency) { + let netCpm = cpm + + if (typeof currency === 'string' && currency.toUpperCase() !== CURRENCY_USD) { + if (typeof getGlobal().convertCurrency === 'function') { + netCpm = parseFloat(Number(getGlobal().convertCurrency(cpm, currency, CURRENCY_USD))).toFixed(3); + } else { + netCpm = null + } + } + return netCpm +} + +function getCurrencyData(bid) { + return { + netCpm: getUsdCpm(bid.cpm, bid.currency), + orginalCpm: getUsdCpm(bid.originalCpm, bid.originalCurrency) + } +} + +/** + * sendRequest to Adagio. It filter null values and encode each query param. + * @param {Object} qp + */ +function sendRequest(qp) { + // Removing null values + qp = Object.keys(qp).reduce((acc, key) => { + if (qp[key] !== null) { + acc[key] = qp[key]; + } + return acc; + }, {}); + + const url = `${ENDPOINT}?${Object.keys(qp).map(key => `${key}=${enc(qp[key])}`).join('&')}`; + ajax(url, null, null, {method: 'GET'}); +}; + +/** + * Send a new beacon to Adagio. It increment the version of the beacon. + * @param {string} auctionId + * @param {string} adUnitCode + */ +function sendNewBeacon(auctionId, adUnitCode) { + cache.updateAuction(auctionId, adUnitCode, { + v: (cache.getAuction(auctionId, adUnitCode).v || 0) + 1 + }); + sendRequest(cache.getAuction(auctionId, adUnitCode)); +}; + +function getTargetedAuctionId(bid) { + return deepAccess(bid, 'latestTargetedAuctionId') || deepAccess(bid, 'auctionId'); +} + +/** + * END UTILS FUNCTIONS + */ + +/** + * HANDLERS + * - handlerAuctionInit + * - handlerBidResponse + * - handlerAuctionEnd + * - handlerBidWon + * - handlerAdRender + * + * Each handler is called when the event is fired. + */ + +function handlerAuctionInit(event) { + const w = getCurrentWindow(); + + const prebidAuctionId = event.auctionId; + const adUnitCodes = removeDuplicates(event.adUnitCodes, adUnitCode => adUnitCode); + + // Check if Adagio is on the bid requests. + // If not, we don't need to track the auction. + const adagioBidRequest = event.bidderRequests.find(bidRequest => isAdagio(bidRequest.bidderCode)); + if (!adagioBidRequest) { + logInfo(`Adagio is not on the bid requests for auction '${prebidAuctionId}'`) + return; + } + + cache.auctions[prebidAuctionId] = {}; + + adUnitCodes.forEach(adUnitCode => { + const adUnits = event.adUnits.filter(adUnit => adUnit.code === adUnitCode); + + // Get all bidders configures for the ad unit. + const bidders = removeDuplicates( + adUnits.map(adUnit => adUnit.bids.map(bid => ({bidder: bid.bidder, params: bid.params}))).flat(), + bidder => bidder.bidder + ); + + // Check if Adagio is configured for the ad unit. + // If not, we don't need to track the ad unit. + const adagioBidder = bidders.find(bidder => isAdagio(bidder.bidder)); + if (!adagioBidder) { + logInfo(`Adagio is not configured for ad unit '${adUnitCode}'`); + return; + } + + // Get all media types and banner sizes configured for the ad unit. + const mediaTypes = adUnits.map(adUnit => adUnit.mediaTypes); + const mediaTypesKeys = removeDuplicates( + mediaTypes.map(mediaTypeObj => Object.keys(mediaTypeObj)).flat(), + mediaTypeKey => mediaTypeKey + ).map(mediaType => getMediaTypeAlias(mediaType)).sort(); + const bannerSizes = removeDuplicates( + mediaTypes.filter(mediaType => mediaType.hasOwnProperty(BANNER)) + .map(mediaType => mediaType[BANNER].sizes.map(size => size.join('x'))) + .flat(), + bannerSize => bannerSize + ).sort(); + + // Get all Adagio bids for the ad unit from the bidRequest. + // If no bids, we don't need to track the ad unit. + const adagioAdUnitBids = adagioBidRequest.bids.filter(bid => bid.adUnitCode === adUnitCode); + if (deepAccess(adagioAdUnitBids, 'length', 0) <= 0) { + logInfo(`Adagio is not on the bid requests for ad unit '${adUnitCode}' and auction '${prebidAuctionId}'`) + return; + } + // Get Adagio params from the first bid. + // We assume that all Adagio bids for a same adunit have the same params. + const params = adagioAdUnitBids[0].params; + + const adagioAuctionId = params.adagioAuctionId; + cache.addPrebidAuctionIdRef(prebidAuctionId, adagioAuctionId); + + // Get all media types requested for Adagio. + const adagioMediaTypes = removeDuplicates( + adagioAdUnitBids.map(bid => Object.keys(bid.mediaTypes)).flat(), + mediaTypeKey => mediaTypeKey + ).flat().map(mediaType => getMediaTypeAlias(mediaType)).sort(); + + const qp = { + v: 0, + pbjsv: PREBID_VERSION, + org_id: params.organizationId, + site: params.site, + pv_id: params.pageviewId, + auct_id: adagioAuctionId, + adu_code: adUnitCode, + url_dmn: w.location.hostname, + pgtyp: params.pagetype, + plcmt: params.placement, + t_n: params.testName || null, + t_v: params.testVersion || null, + mts: mediaTypesKeys.join(','), + ban_szs: bannerSizes.join(','), + bdrs: bidders.map(bidder => bidder.bidder).sort().join(','), + adg_mts: adagioMediaTypes.join(',') + }; + + cache.auctions[prebidAuctionId][adUnitCode] = qp; + sendNewBeacon(prebidAuctionId, adUnitCode); + }); +}; + +/** + * handlerBidResponse allow to track the adagio bid response + * and to update the auction cache with the seat ID. + * No beacon is sent here. + */ +function handlerBidResponse(event) { + if (!guard.adagio(event.bidder)) { + return; + } + + if (!guard.bidTracked(event.auctionId, event.adUnitCode)) { + return; + } + + if (!event.pba) { + return; + } + + cache.updateAuction(event.auctionId, event.adUnitCode, { + ...addKeyPrefix(event.pba, 'e_') + }); +}; + +function handlerAuctionEnd(event) { + const { auctionId } = event; + + if (!guard.auctionTracked(auctionId)) { + return; + } + + const adUnitCodes = cache.getAllAdUnitCodes(auctionId); + adUnitCodes.forEach(adUnitCode => { + const bidResponseMapper = (bidder) => { + const bid = event.bidsReceived.find(bid => bid.adUnitCode === adUnitCode && bid.bidder === bidder) + return bid ? '1' : '0' + } + const bidCpmMapper = (bidder) => { + const bid = event.bidsReceived.find(bid => bid.adUnitCode === adUnitCode && bid.bidder === bidder) + return bid ? getCurrencyData(bid).netCpm : null + } + + cache.updateAuction(auctionId, adUnitCode, { + bdrs_bid: cache.getBiddersFromAuction(auctionId, adUnitCode).map(bidResponseMapper).join(','), + bdrs_cpm: cache.getBiddersFromAuction(auctionId, adUnitCode).map(bidCpmMapper).join(',') + }); + sendNewBeacon(auctionId, adUnitCode); + }); } +function handlerBidWon(event) { + let auctionId = getTargetedAuctionId(event); + + if (!guard.bidTracked(auctionId, event.adUnitCode)) { + return; + } + + const currencyData = getCurrencyData(event) + + const adagioAuctionCacheId = ( + (event.latestTargetedAuctionId && event.latestTargetedAuctionId !== event.auctionId) + ? cache.getAdagioAuctionId(event.auctionId) + : null); + + cache.updateAuction(auctionId, event.adUnitCode, { + win_bdr: event.bidder, + win_mt: getMediaTypeAlias(event.mediaType), + win_ban_sz: event.mediaType === BANNER ? `${event.width}x${event.height}` : null, + + win_net_cpm: currencyData.netCpm, + win_og_cpm: currencyData.orginalCpm, + + // cache bid id + auct_id_c: adagioAuctionCacheId, + }); + sendNewBeacon(auctionId, event.adUnitCode); +}; + +function handlerAdRender(event, isSuccess) { + const { adUnitCode } = event.bid; + let auctionId = getTargetedAuctionId(event.bid); + + if (!guard.bidTracked(auctionId, adUnitCode)) { + return; + } + + cache.updateAuction(auctionId, adUnitCode, { + rndr: isSuccess ? 1 : 0 + }); + sendNewBeacon(auctionId, adUnitCode); +}; + +/** + * END HANDLERS + */ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { - track: function({ eventType, args }) { - if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { - adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + track: function(event) { + const { eventType, args } = event; + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + handlerAuctionInit(args); + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + handlerBidResponse(args); + break; + case CONSTANTS.EVENTS.AUCTION_END: + handlerAuctionEnd(args); + break; + case CONSTANTS.EVENTS.BID_WON: + handlerBidWon(args); + break; + // AD_RENDER_SUCCEEDED seems redundant with BID_WON. + // case CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED: + case CONSTANTS.EVENTS.AD_RENDER_FAILED: + handlerAdRender(args, eventType === CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED); + break; + } + } catch (error) { + logError('Error on Adagio Analytics Adapter', error); + } + + try { + if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { + adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - adagio.js', error); } } }); @@ -37,11 +418,8 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { adagioAdapter.originEnableAnalytics = adagioAdapter.enableAnalytics; adagioAdapter.enableAnalytics = config => { - if (!canAccessTopWindow()) { - return; - } - - const w = getWindowTop(); + const w = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf(); + currentWindow = w; w.ADAGIO = w.ADAGIO || {}; w.ADAGIO.queue = w.ADAGIO.queue || []; @@ -53,7 +431,8 @@ adagioAdapter.enableAnalytics = config => { adapterManager.registerAnalyticsAdapter({ adapter: adagioAdapter, - code: 'adagio' + code: ADAGIO_CODE, + gvlid: ADAGIO_GVLID, }); export default adagioAdapter; diff --git a/modules/adagioAnalyticsAdapter.md b/modules/adagioAnalyticsAdapter.md index 312a26ea8da..9fc2cb0bb88 100644 --- a/modules/adagioAnalyticsAdapter.md +++ b/modules/adagioAnalyticsAdapter.md @@ -8,10 +8,10 @@ Maintainer: dev@adagio.io Analytics adapter for Adagio -# Test Parameters +# Settings -``` -{ - provider: 'adagio' -} +```js + pbjs.enableAnalytics({ + provider: 'adagio', + }); ``` diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index a3369ec3357..6e3c38e4e85 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,12 +1,10 @@ import {find} from '../src/polyfill.js'; import { - _map, cleanObj, deepAccess, deepClone, generateUUID, getDNT, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, getWindowSelf, getWindowTop, @@ -34,6 +32,7 @@ import {OUTSTREAM} from '../src/video.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { userSync } from '../src/userSync.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; @@ -54,8 +53,9 @@ const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0k const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -// This provide a whitelist and a basic validation of OpenRTB 2.6 options used by the Adagio SSP. -// https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf +// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP. +// Accept all options but 'protocol', 'companionad', 'companiontype', 'ext' +// 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) => isInteger(value), @@ -77,7 +77,7 @@ export const ORTB_VIDEO_PARAMS = { 'boxingallowed': (value) => isInteger(value), 'playbackmethod': (value) => isArrayOfNums(value), 'playbackend': (value) => isInteger(value), - 'delivery': (value) => isInteger(value), + 'delivery': (value) => isArrayOfNums(value), 'pos': (value) => isInteger(value), 'api': (value) => isArrayOfNums(value) }; @@ -399,6 +399,17 @@ function _getUspConsent(bidderRequest) { return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false; } +function _getGppConsent(bidderRequest) { + let gpp = deepAccess(bidderRequest, 'gppConsent.gppString') + let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections') + + if (!gpp || !gppSid) { + gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '') + gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []) + } + return { gpp, gppSid } +} + function _getSchain(bidRequest) { return deepAccess(bidRequest, 'schain'); } @@ -558,6 +569,7 @@ function _parseNativeBidResponse(bid) { bid.native = native } +// bidRequest param must be the `bidRequest` object with the original `auctionId` value. function _getFloors(bidRequest) { if (!isFn(bidRequest.getFloor)) { return false; @@ -688,9 +700,9 @@ function getPageDimensions() { } /** -* @todo Move to prebid Core as Utils. -* @returns -*/ + * @todo Move to prebid Core as Utils. + * @returns + */ function getViewPortDimensions() { if (!isSafeFrameWindow() && !canAccessTopWindow()) { return ''; @@ -783,16 +795,12 @@ function getSlotPosition(adUnitElementId) { const scrollLeft = wt.pageXOffset || docEl.scrollLeft || body.scrollLeft; const elComputedStyle = wt.getComputedStyle(domElement, null); - const elComputedDisplay = elComputedStyle.display || 'block'; - const mustDisplayElement = elComputedDisplay === 'none'; + const mustDisplayElement = elComputedStyle.display === 'none'; if (mustDisplayElement) { - domElement.style = domElement.style || {}; - const originalDisplay = domElement.style.display; - domElement.style.display = 'block'; - box = domElement.getBoundingClientRect(); - domElement.style.display = originalDisplay || null; + logWarn(LOG_PREFIX, 'The element is hidden. The slot position cannot be computed.'); } + position.x = Math.round(box.left + scrollLeft - clientLeft); position.y = Math.round(box.top + scrollTop - clientTop); } catch (err) { @@ -820,8 +828,8 @@ function getPrintNumber(adUnitCode, bidderRequest) { } /** - * domLoading feature is computed on window.top if reachable. - */ + * domLoading feature is computed on window.top if reachable. + */ function getDomLoadingDuration() { let domLoadingDuration = -1; let performance; @@ -976,15 +984,19 @@ export const spec = { const gdprConsent = _getGdprConsent(bidderRequest) || {}; const uspConsent = _getUspConsent(bidderRequest) || {}; const coppa = _getCoppa(); + const gppConsent = _getGppConsent(bidderRequest) const schain = _getSchain(validBidRequests[0]); const eids = _getEids(validBidRequests[0]) || []; const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') const usIfr = syncEnabled && userSync.canBidderRegisterSync('iframe', 'adagio') + // We don't validate the dsa object in adapter and let our server do it. + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + const aucId = generateUUID() - const adUnits = _map(validBidRequests, (rawBidRequest) => { - const bidRequest = {...rawBidRequest} + const adUnits = validBidRequests.map(rawBidRequest => { + const bidRequest = deepClone(rawBidRequest); // Fix https://github.com/prebid/Prebid.js/issues/9781 bidRequest.auctionId = aucId @@ -1007,6 +1019,9 @@ export const spec = { } } + // Enforce the organizationId param to be a string + bidRequest.params.organizationId = bidRequest.params.organizationId.toString(); + // Force the Data Layer key and value to be a String if (bidRequest.params.dataLayer) { if (isStr(bidRequest.params.dataLayer) || isNumber(bidRequest.params.dataLayer) || isArray(bidRequest.params.dataLayer) || isFn(bidRequest.params.dataLayer)) { @@ -1055,7 +1070,10 @@ export const spec = { }); // Handle priceFloors module - const computedFloors = _getFloors(bidRequest); + // We need to use `rawBidRequest` as param because: + // - adagioBidAdapter generates its own auctionId due to transmitTid activity limitation (see https://github.com/prebid/Prebid.js/pull/10079) + // - the priceFloors.getFloor() uses a `_floorDataForAuction` map to store the floors based on the auctionId. + const computedFloors = _getFloors(rawBidRequest); if (isArray(computedFloors) && computedFloors.length) { bidRequest.floors = computedFloors @@ -1099,36 +1117,55 @@ export const spec = { _buildVideoBidRequest(bidRequest); } + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + bidRequest.gpid = gpid; + } + + // store the whole bidRequest (adUnit) object in the ADAGIO namespace. storeRequestInAdagioNS(bidRequest); - // Remove these fields at the very end, so we can still use them before. - delete bidRequest.transactionId; - delete bidRequest.ortb2Imp; - delete bidRequest.ortb2; - delete bidRequest.sizes; + // Remove some params that are not needed on the server side. + delete bidRequest.params.siteId; + + // whitelist the fields that are allowed to be sent to the server. + const adUnit = { + adUnitCode: bidRequest.adUnitCode, + auctionId: bidRequest.auctionId, + bidder: bidRequest.bidder, + bidId: bidRequest.bidId, + params: bidRequest.params, + features: bidRequest.features, + gpid: bidRequest.gpid, + mediaTypes: bidRequest.mediaTypes, + nativeParams: bidRequest.nativeParams, + score: bidRequest.score, + transactionId: bidRequest.transactionId, + } - return bidRequest; + return adUnit; }); // Group ad units by organizationId const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => { - const adUnitCopy = deepClone(adUnit); - adUnitCopy.params.organizationId = adUnitCopy.params.organizationId.toString(); + const organizationId = adUnit.params.organizationId - // remove useless props - delete adUnitCopy.floorData; - delete adUnitCopy.params.siteId; - delete adUnitCopy.userId - delete adUnitCopy.userIdAsEids - - groupedAdUnits[adUnitCopy.params.organizationId] = groupedAdUnits[adUnitCopy.params.organizationId] || []; - groupedAdUnits[adUnitCopy.params.organizationId].push(adUnitCopy); + groupedAdUnits[organizationId] = groupedAdUnits[organizationId] || []; + groupedAdUnits[organizationId].push(adUnit); return groupedAdUnits; }, {}); + // Adding more params on the original bid object. + // Those params are not sent to the server. + // They are used for further operations on analytics adapter. + validBidRequests.forEach(rawBidRequest => { + rawBidRequest.params.adagioAuctionId = aucId + rawBidRequest.params.pageviewId = pageviewId + }); + // Build one request per organizationId - const requests = _map(Object.keys(groupedAdUnits), organizationId => { + const requests = Object.keys(groupedAdUnits).map(organizationId => { return { method: 'POST', url: ENDPOINT, @@ -1143,7 +1180,10 @@ export const spec = { regs: { gdpr: gdprConsent, coppa: coppa, - ccpa: uspConsent + ccpa: uspConsent, + gpp: gppConsent.gpp, + gppSid: gppConsent.gppSid, + dsa: dsa // populated if exists }, schain: schain, user: { @@ -1180,6 +1220,7 @@ export const spec = { const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId)); if (bidReq) { + // bidObj.meta is the `bidResponse.meta` object according to https://docs.prebid.org/dev-docs/bidder-adaptor.html#interpreting-the-response bidObj.meta = deepAccess(bidObj, 'meta', {}); bidObj.meta.mediaType = bidObj.mediaType; bidObj.meta.advertiserDomains = (Array.isArray(bidObj.aDomain) && bidObj.aDomain.length) ? bidObj.aDomain : []; diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 45f39fc6f2d..19673571982 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -107,10 +107,11 @@ var adUnits = [ cpm: 3.00 // default to 1.00 }, video: { - api: [2, 7], // Required - Your video player must at least support the value 2 and/or 7. + api: [2], // Required - Your video player must at least support the value 2 playbackMethod: [6], // Highly recommended skip: 0 - // OpenRTB video options defined here override ones defined in mediaTypes. + // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. + // Not supported: 'protocol', 'companionad', 'companiontype', 'ext' }, native: { // Optional OpenRTB Native 1.2 request object. Only `context`, `plcmttype` fields are supported. @@ -193,6 +194,8 @@ If the FPD value is an array, the 1st value of this array will be used. placement: 'in_article', adUnitElementId: 'article_outstream', video: { + api: [2], + playbackMethod: [6], skip: 0 }, debug: { diff --git a/modules/adbutlerBidAdapter.js b/modules/adbutlerBidAdapter.js new file mode 100644 index 00000000000..de430a5c916 --- /dev/null +++ b/modules/adbutlerBidAdapter.js @@ -0,0 +1,113 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adbutler'; + +function getTrackingPixelsMarkup(pixelURLs) { + return pixelURLs + .map(pixelURL => ``) + .join(); +} + +export const spec = { + code: BIDDER_CODE, + pageID: Math.floor(Math.random() * 10e6), + aliases: ['divreach', 'doceree'], + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + return !!(bid.params.accountID && bid.params.zoneID); + }, + + buildRequests(validBidRequests) { + const zoneCounters = {}; + + return utils._map(validBidRequests, function (bidRequest) { + const zoneID = bidRequest.params?.zoneID; + + zoneCounters[zoneID] ??= 0; + + const domain = bidRequest.params?.domain ?? 'servedbyadbutler.com'; + const adserveBase = `https://${domain}/adserve`; + const params = { + ...(bidRequest.params?.extra ?? {}), + ID: bidRequest.params?.accountID, + type: 'hbr', + setID: zoneID, + pid: spec.pageID, + place: zoneCounters[zoneID], + kw: bidRequest.params?.keyword, + }; + + const paramsString = Object.entries(params).map(([key, value]) => `${key}=${value}`).join(';'); + const requestURI = `${adserveBase}/;${paramsString};`; + + zoneCounters[zoneID]++; + + return { + method: 'GET', + url: requestURI, + data: {}, + bidRequest, + }; + }); + }, + + interpretResponse(serverResponse, serverRequest) { + const bidObj = serverRequest.bidRequest; + const response = serverResponse.body ?? {}; + + if (!bidObj || response.status !== 'SUCCESS') { + return []; + } + + const width = parseInt(response.width); + const height = parseInt(response.height); + + const sizeValid = (bidObj.mediaTypes?.banner?.sizes ?? []).some(([w, h]) => w === width && h === height); + + if (!sizeValid) { + return []; + } + + const cpm = response.cpm; + const minCPM = bidObj.params?.minCPM ?? null; + const maxCPM = bidObj.params?.maxCPM ?? null; + + if (minCPM !== null && cpm < minCPM) { + return []; + } + + if (maxCPM !== null && cpm > maxCPM) { + return []; + } + + let advertiserDomains = []; + + if (response.advertiser?.domain) { + advertiserDomains.push(response.advertiser.domain); + } + + const bidResponse = { + requestId: bidObj.bidId, + cpm, + currency: 'USD', + width, + height, + ad: response.ad_code + getTrackingPixelsMarkup(response.tracking_pixels), + ttl: 360, + creativeId: response.placement_id, + netRevenue: true, + meta: { + advertiserId: response.advertiser?.id, + advertiserName: response.advertiser?.name, + advertiserDomains, + }, + }; + + return [bidResponse]; + }, +}; + +registerBidder(spec); diff --git a/modules/adbutlerBidAdapter.md b/modules/adbutlerBidAdapter.md new file mode 100644 index 00000000000..88b5cf64475 --- /dev/null +++ b/modules/adbutlerBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +**Module Name**: AdButler Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: trevor@sparklit.com + +# Description + +Bid Adapter for creating a bid from an AdButler zone. + +# Test Parameters +``` + var adUnits = [ + { + code: 'display-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "adbutler", + params: { + accountID: '181556', + zoneID: '705374', + keyword: 'red', //optional + minCPM: '1.00', //optional + maxCPM: '5.00' //optional + } + } + ] + } + ]; +``` diff --git a/modules/adfBidAdapter.js b/modules/adfBidAdapter.js index 5c4b03c3bb2..881b1adfcc4 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -64,13 +64,17 @@ export const spec = { const cur = currency && [ currency ]; const eids = setOnAny(validBidRequests, 'userIdAsEids'); const schain = setOnAny(validBidRequests, 'schain'); + const dsa = commonFpd.regs?.ext?.dsa; const imp = validBidRequests.map((bid, id) => { bid.netRevenue = pt; const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' + currency: currency || 'USD', + size: '*', + mediaType: '*' }) : {}; + const bidfloor = floorInfo.floor; const bidfloorcur = floorInfo.currency; const { mid, inv, mname } = bid.params; @@ -176,6 +180,10 @@ export const spec = { deepSetValue(request, 'source.ext.schain', schain); } + if (dsa) { + deepSetValue(request, 'regs.ext.dsa', dsa); + } + return { method: 'POST', url: 'https://' + adxDomain + '/adx/openrtb', @@ -198,6 +206,7 @@ export const spec = { const bidResponse = bidResponses[id]; if (bidResponse) { const mediaType = deepAccess(bidResponse, 'ext.prebid.type'); + const dsa = deepAccess(bidResponse, 'ext.dsa'); const result = { requestId: bid.bidId, cpm: bidResponse.price, @@ -211,7 +220,8 @@ export const spec = { dealId: bidResponse.dealid, meta: { mediaType, - advertiserDomains: bidResponse.adomain + advertiserDomains: bidResponse.adomain, + dsa } }; @@ -220,7 +230,14 @@ export const spec = { ortb: bidResponse.native }; } else { - result[ mediaType === VIDEO ? 'vastXml' : 'ad' ] = bidResponse.adm; + if (mediaType === VIDEO) { + result.vastXml = bidResponse.adm; + if (bidResponse.nurl) { + result.vastUrl = bidResponse.nurl; + } + } else { + result.ad = bidResponse.adm; + } } if (!bid.renderer && mediaType === VIDEO && deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { diff --git a/modules/adfusionBidAdapter.js b/modules/adfusionBidAdapter.js new file mode 100644 index 00000000000..a206ee5e899 --- /dev/null +++ b/modules/adfusionBidAdapter.js @@ -0,0 +1,120 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const adpterVersion = '1.0'; +export const REQUEST_URL = 'https://spicyrtb.com/auction/prebid'; +export const DEFAULT_CURRENCY = 'USD'; + +export const spec = { + code: 'adfusion', + gvlid: 844, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + isBannerBid, + isVideoBid, +}; + +registerBidder(spec); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: DEFAULT_CURRENCY, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const floor = getBidFloor(bidRequest); + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + utils.mergeDeep(req, { + at: 1, + ext: { + prebid: { + accountid: bid.params.accountId, + adapterVersion: `${adpterVersion}`, + }, + }, + }); + return req; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids; + }, +}); + +function isBidRequestValid(bidRequest) { + const isValid = bidRequest.params.accountId; + if (!isValid) { + utils.logError('AdFusion adapter bidRequest has no accountId'); + return false; + } + return true; +} + +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter((bid) => isVideoBid(bid)); + let bannerBids = bids.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: REQUEST_URL, + data: converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }), + }; +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner'); +} + +function interpretResponse(resp, req) { + return converter.fromORTB({ request: req.data, response: resp.body }); +} + +function getBidFloor(bid) { + if (utils.isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + if ( + utils.isPlainObject(floor) && + !isNaN(floor.floor) && + floor.currency === DEFAULT_CURRENCY + ) { + return floor.floor; + } + } + return null; +} diff --git a/modules/adfusionBidAdapter.md b/modules/adfusionBidAdapter.md new file mode 100644 index 00000000000..803a03ba1a1 --- /dev/null +++ b/modules/adfusionBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: AdFusion Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@adfusion.pl +``` + +# Description + +Module that connects to AdFusion demand sources + +# Banner Test Parameters + +```js +var adUnits = [ + { + code: "test-banner", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + [320, 480], + ], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], + }, +]; +``` + +# Video Test Parameters + +```js +var videoAdUnit = { + code: "video1", + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 480], + mimes: ["video/mp4"], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], +}; +``` diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index f18b7629d8f..e0538fe2815 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -1,8 +1,18 @@ -import {tryAppendQueryString, getBidIdParameter, escapeUnsafeChars, deepAccess} from '../src/utils.js'; +import {deepAccess, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {escapeUnsafeChars} from '../libraries/htmlEscape/htmlEscape.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const ADG_BIDDER_CODE = 'adgeneration'; @@ -28,7 +38,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const ADGENE_PREBID_VERSION = '1.6.0'; + const ADGENE_PREBID_VERSION = '1.6.2'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -41,6 +51,7 @@ export const spec = { const imuid = deepAccess(validReq, 'userId.imuid'); const gpid = deepAccess(validReq, 'ortb2Imp.ext.gpid'); const sua = deepAccess(validReq, 'ortb2.device.sua'); + const uid2 = deepAccess(validReq, 'userId.uid2.id'); let data = ``; data = tryAppendQueryString(data, 'posall', 'SSPLOC'); const id = getBidIdParameter('id', validReq.params); @@ -58,10 +69,10 @@ export const spec = { data = tryAppendQueryString(data, 'adgext_id5_id', id5id); data = tryAppendQueryString(data, 'adgext_id5_id_link_type', id5LinkType); data = tryAppendQueryString(data, 'adgext_imuid', imuid); - data = tryAppendQueryString(data, 'adgext_uid2', validReq.userId ? validReq.userId.uid2 : null); - data = tryAppendQueryString(data, 'gpid', gpid ? encodeURIComponent(gpid) : null); + data = tryAppendQueryString(data, 'adgext_uid2', uid2); + data = tryAppendQueryString(data, 'gpid', gpid); data = tryAppendQueryString(data, 'uach', sua ? JSON.stringify(sua) : null); - data = tryAppendQueryString(data, 'schain', validReq.schain ? encodeURIComponent(JSON.stringify(validReq.schain)) : null); + data = tryAppendQueryString(data, 'schain', validReq.schain ? JSON.stringify(validReq.schain) : null); // nativeäģĨ外ãĢvideoį­‰ãŽå¯žåŋœãŒå…ĨãŖた場合はčĻäŋŽæ­Ŗ if (!validReq.mediaTypes || !validReq.mediaTypes.native) { diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 4db46aca3c6..81067a3efcf 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -159,7 +159,7 @@ export const spec = { code: 'adkernelAdn', gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], - aliases: ['engagesimply'], + aliases: ['engagesimply', 'adpluto_dsp'], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 64567832dbd..ae02a8967b1 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,11 +1,9 @@ import { _each, - cleanObj, contains, createTrackPixelHtml, deepAccess, deepSetValue, - getAdUnitSizes, getDefinedParams, getDNT, isArray, @@ -21,50 +19,37 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; -/* +/** * In case you're AdKernel whitelable platform's client who needs branded adapter to * work with Adkernel platform - DO NOT COPY THIS ADAPTER UNDER NEW NAME * - * Please contact prebid@adkernel.com and we'll add your adapter as an alias. + * Please contact prebid@adkernel.com and we'll add your adapter as an alias + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync */ -const VIDEO_PARAMS = ['pos', 'context', 'placement', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', + +const VIDEO_PARAMS = ['pos', 'context', 'placement', 'plcmt', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', 'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed']; const VIDEO_FPD = ['battr', 'pos']; const NATIVE_FPD = ['battr', 'api']; +const BANNER_PARAMS = ['pos']; const BANNER_FPD = ['btype', 'battr', 'pos', 'api']; -const VERSION = '1.6'; +const VERSION = '1.7'; const SYNC_IFRAME = 1; const SYNC_IMAGE = 2; -const SYNC_TYPES = Object.freeze({ +const SYNC_TYPES = { 1: 'iframe', 2: 'image' -}); +}; const GVLID = 14; -const NATIVE_MODEL = [ - {name: 'title', assetType: 'title'}, - {name: 'icon', assetType: 'img', type: 1}, - {name: 'image', assetType: 'img', type: 3}, - {name: 'body', assetType: 'data', type: 2}, - {name: 'body2', assetType: 'data', type: 10}, - {name: 'sponsoredBy', assetType: 'data', type: 1}, - {name: 'phone', assetType: 'data', type: 8}, - {name: 'address', assetType: 'data', type: 9}, - {name: 'price', assetType: 'data', type: 6}, - {name: 'salePrice', assetType: 'data', type: 7}, - {name: 'cta', assetType: 'data', type: 12}, - {name: 'rating', assetType: 'data', type: 3}, - {name: 'downloads', assetType: 'data', type: 5}, - {name: 'likes', assetType: 'data', type: 4}, - {name: 'displayUrl', assetType: 'data', type: 11} -]; - -const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { - acc[val.name] = {id: idx, ...val}; - return acc; -}, {}); +const MULTI_FORMAT_SUFFIX = '__mf'; +const MULTI_FORMAT_SUFFIX_BANNER = 'b' + MULTI_FORMAT_SUFFIX; +const MULTI_FORMAT_SUFFIX_VIDEO = 'v' + MULTI_FORMAT_SUFFIX; +const MULTI_FORMAT_SUFFIX_NATIVE = 'n' + MULTI_FORMAT_SUFFIX; /** * Adapter for requesting bids from AdKernel white-label display platform @@ -98,9 +83,12 @@ export const spec = { {code: 'displayioads'}, {code: 'rtbdemand_com'}, {code: 'bidbuddy'}, - {code: 'adliveconnect'}, {code: 'didnadisplay'}, - {code: 'qortex'} + {code: 'qortex'}, + {code: 'adpluto'}, + {code: 'headbidder'}, + {code: 'digiad'}, + {code: 'monetix'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -126,9 +114,6 @@ export const spec = { * @returns {ServerRequest[]} */ buildRequests: function (bidRequests, bidderRequest) { - // convert Native ORTB definition to old-style prebid native definition - bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); - let impGroups = groupImpressionsByHostZone(bidRequests, bidderRequest.refererInfo); let requests = []; let schain = bidRequests[0].schain; @@ -171,6 +156,9 @@ export const spec = { ttl: 360, netRevenue: true }; + if (prBid.requestId.endsWith(MULTI_FORMAT_SUFFIX)) { + prBid.requestId = stripMultiformatSuffix(prBid.requestId); + } if ('banner' in imp) { prBid.mediaType = BANNER; prBid.width = rtbBid.w; @@ -183,7 +171,9 @@ export const spec = { prBid.height = imp.video.h; } else if ('native' in imp) { prBid.mediaType = NATIVE; - prBid.native = buildNativeAd(JSON.parse(rtbBid.adm)); + prBid.native = { + ortb: buildNativeAd(rtbBid.adm) + }; } if (isStr(rtbBid.dealid)) { prBid.dealId = rtbBid.dealid; @@ -237,13 +227,13 @@ registerBidder(spec); function groupImpressionsByHostZone(bidRequests, refererInfo) { let secure = (refererInfo && refererInfo.page?.indexOf('https:') === 0); return Object.values( - bidRequests.map(bidRequest => buildImp(bidRequest, secure)) + bidRequests.map(bidRequest => buildImps(bidRequest, secure)) .reduce((acc, curr, index) => { let bidRequest = bidRequests[index]; let {zoneId, host} = bidRequest.params; let key = `${host}_${zoneId}`; acc[key] = acc[key] || {host: host, zoneId: zoneId, imps: []}; - acc[key].imps.push(curr); + acc[key].imps.push(...curr); return acc; }, {}) ); @@ -262,115 +252,95 @@ function getBidFloor(bid, mediaType, sizes) { } /** - * Builds rtb imp object for single adunit + * Builds rtb imp object(s) for single adunit * @param bidRequest {BidRequest} * @param secure {boolean} */ -function buildImp(bidRequest, secure) { - const imp = { +function buildImps(bidRequest, secure) { + let imp = { 'id': bidRequest.bidId, 'tagid': bidRequest.adUnitCode }; - var mediaType; + if (secure) { + imp.secure = 1; + } var sizes = []; - - if (deepAccess(bidRequest, 'mediaTypes.banner')) { + let mediaTypes = bidRequest.mediaTypes; + let isMultiformat = (~~!!mediaTypes?.banner + ~~!!mediaTypes?.video + ~~!!mediaTypes?.native) > 1; + let result = []; + let typedImp; + + if (mediaTypes?.banner) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = imp.id + MULTI_FORMAT_SUFFIX_BANNER; + } else { + typedImp = imp; + } sizes = getAdUnitSizes(bidRequest); - imp.banner = { + let pbBanner = mediaTypes.banner; + typedImp.banner = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, BANNER_FPD), + ...getDefinedParamsOrEmpty(pbBanner, BANNER_PARAMS), format: sizes.map(wh => parseGPTSingleSizeArrayToRtbSize(wh)), topframe: 0 }; - populateImpFpd(imp.banner, bidRequest, BANNER_FPD); - mediaType = BANNER; - } else if (deepAccess(bidRequest, 'mediaTypes.video')) { - let video = deepAccess(bidRequest, 'mediaTypes.video'); - imp.video = getDefinedParams(video, VIDEO_PARAMS); - populateImpFpd(imp.video, bidRequest, VIDEO_FPD); - if (video.playerSize) { - sizes = video.playerSize[0]; - imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; - } - mediaType = VIDEO; - } else if (deepAccess(bidRequest, 'mediaTypes.native')) { - let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); - imp.native = { - ver: '1.1', - request: JSON.stringify(nativeRequest) - }; - populateImpFpd(imp.native, bidRequest, NATIVE_FPD); - mediaType = NATIVE; - } else { - throw new Error('Unsupported bid received'); - } - let floor = getBidFloor(bidRequest, mediaType, sizes); - if (floor) { - imp.bidfloor = floor; + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : BANNER); + result.push(typedImp); } - if (secure) { - imp.secure = 1; - } - return imp; -} -/** - * Builds native request from native adunit - */ -function buildNativeRequest(nativeReq) { - let request = {ver: '1.1', assets: []}; - for (let k of Object.keys(nativeReq)) { - let v = nativeReq[k]; - let desc = NATIVE_INDEX[k]; - if (desc === undefined) { - continue; + if (mediaTypes?.video) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_VIDEO; + } else { + typedImp = imp; } - let assetRoot = { - id: desc.id, - required: ~~v.required, + let pbVideo = mediaTypes.video; + typedImp.video = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, VIDEO_FPD), + ...getDefinedParamsOrEmpty(pbVideo, VIDEO_PARAMS) }; - if (desc.assetType === 'img') { - assetRoot[desc.assetType] = buildImageAsset(desc, v); - } else if (desc.assetType === 'data') { - assetRoot.data = cleanObj({type: desc.type, len: v.len}); - } else if (desc.assetType === 'title') { - assetRoot.title = {len: v.len || 90}; + if (pbVideo.playerSize) { + sizes = pbVideo.playerSize[0]; + typedImp.video = Object.assign(typedImp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); + } else if (pbVideo.w && pbVideo.h) { + typedImp.video.w = pbVideo.w; + typedImp.video.h = pbVideo.h; + } + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : VIDEO); + result.push(typedImp); + } + + if (mediaTypes?.native) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_NATIVE; } else { - return; + typedImp = imp; } - request.assets.push(assetRoot); + typedImp.native = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, NATIVE_FPD), + request: JSON.stringify(bidRequest.nativeOrtbRequest) + }; + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : NATIVE); + result.push(typedImp); } - return request; + return result; } -/** - * Populate impression-level FPD from bid request - * @param target {Object} - * @param bidRequest {BidRequest} - * @param props {String[]} - */ -function populateImpFpd(target, bidRequest, props) { - if (bidRequest.ortb2Imp === undefined) { - return; +function initImpBidfloor(imp, bid, sizes, mediaType) { + let bidfloor = getBidFloor(bid, mediaType, sizes); + if (bidfloor) { + imp.bidfloor = bidfloor; } - Object.assign(target, getDefinedParams(bidRequest.ortb2Imp, props)); } -/** - * Builds image asset request - */ -function buildImageAsset(desc, val) { - let img = { - type: desc.type - }; - if (val.sizes) { - [img.w, img.h] = val.sizes; - } else if (val.aspect_ratios) { - img.wmin = val.aspect_ratios[0].min_width; - img.hmin = val.aspect_ratios[0].min_height; +function getDefinedParamsOrEmpty(object, params) { + if (object === undefined) { + return {}; } - return cleanObj(img); + return getDefinedParams(object, params); } /** @@ -456,7 +426,9 @@ function makeUser(bidderRequest, fpd) { if (eids) { deepSetValue(user, 'ext.eids', eids); } - if (!isEmpty(user)) { return {user: user}; } + if (!isEmpty(user)) { + return {user: user}; + } } /** @@ -465,13 +437,17 @@ function makeUser(bidderRequest, fpd) { * @returns {{regs: Object} | undefined} */ function makeRegulations(bidderRequest) { - let {gdprConsent, uspConsent} = bidderRequest; + let {gdprConsent, uspConsent, gppConsent} = bidderRequest; let regs = {}; if (gdprConsent) { if (gdprConsent.gdprApplies !== undefined) { deepSetValue(regs, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies); } } + if (gppConsent) { + deepSetValue(regs, 'regs.gpp', gppConsent.gppString); + deepSetValue(regs, 'regs.gpp_sid', gppConsent.applicableSections); + } if (uspConsent) { deepSetValue(regs, 'regs.ext.us_privacy', uspConsent); } @@ -491,12 +467,11 @@ function makeRegulations(bidderRequest) { * @returns */ function makeBaseRequest(bidderRequest, imps, fpd) { - let {timeout} = bidderRequest; let request = { 'id': bidderRequest.bidderRequestId, 'imp': imps, 'at': 1, - 'tmax': parseInt(timeout) + 'tmax': parseInt(bidderRequest.timeout) }; if (!isEmpty(fpd.bcat)) { request.bcat = fpd.bcat; @@ -616,24 +591,17 @@ function validateNativeImageSize(img) { } /** - * Creates native ad for native 1.1 response + * Creates native ad for native 1.2 response */ -function buildNativeAd(nativeResp) { - const {assets, link, imptrackers, jstracker, privacy} = nativeResp.native; - let nativeAd = { - clickUrl: link.url, - impressionTrackers: imptrackers, - javascriptTrackers: jstracker ? [jstracker] : undefined, - privacyLink: privacy, - }; - _each(assets, asset => { - let assetName = NATIVE_MODEL[asset.id].name; - let assetType = NATIVE_MODEL[asset.id].assetType; - nativeAd[assetName] = asset[assetType].text || asset[assetType].value || cleanObj({ - url: asset[assetType].url, - width: asset[assetType].w, - height: asset[assetType].h - }); - }); - return cleanObj(nativeAd); +function buildNativeAd(adm) { + let resp = JSON.parse(adm); + // temporary workaround for top-level native object wrapper + if ('native' in resp) { + resp = resp.native; + } + return resp; +} + +function stripMultiformatSuffix(impid) { + return impid.substr(0, impid.length - MULTI_FORMAT_SUFFIX.length - 1); } diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index 5db75c656bb..9284d543298 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -14,7 +14,6 @@ import {find} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { deepAccess, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, insertElement, isFn, @@ -28,6 +27,7 @@ import { mergeDeep, parseUrl } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'adlooxAnalyticsAdapter'; diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index c2037429185..727dc84e399 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -25,7 +25,6 @@ import { deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, isArray, isBoolean, isInteger, @@ -37,6 +36,7 @@ import { parseUrl, safeJSONParse } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index 2ee6ecfcb56..b78737722bd 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -4,6 +4,7 @@ import { isFn, deepAccess, logMessage } from '../src/utils.js'; import {config} from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +const GVLID = 149; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; const URL_SYNC = 'https://sync.admanmedia.com'; @@ -57,6 +58,7 @@ function getUserId(eids, id, source, uidExt) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -94,7 +96,9 @@ export const spec = { request.ccpa = bidderRequest.uspConsent; } if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; } if (content) { request.content = content; diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 027f924ac5d..6c268b2d382 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -1,16 +1,50 @@ -import { getValue, logError, isEmpty, deepAccess, getBidIdParameter, isArray } from '../src/utils.js'; +import {getValue, formatQS, logError, deepAccess, isArray, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + +export const OPENRTB = { + NATIVE: { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + SPONSORED: 5, + CTA: 6 + }, + DATA_ASSET_TYPE: { + SPONSORED: 1, + DESC: 2, + CTA_TEXT: 12, + }, + } +}; + let SYNC_URL = ''; const BIDDER_CODE = 'admatic'; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + export const spec = { code: BIDDER_CODE, + gvlid: 1281, aliases: [ - {code: 'pixad'} + {code: 'pixad', gvlid: 1281} ], - supportedMediaTypes: [BANNER, VIDEO], - /** f + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + /** + * f * @param {object} bid * @return {boolean} */ @@ -33,23 +67,21 @@ export const spec = { * @return {ServerRequest} */ buildRequests: (validBidRequests, bidderRequest) => { + const tmax = bidderRequest.timeout; const bids = validBidRequests.map(buildRequestObject); - const blacklist = bidderRequest.ortb2; + const ortb = bidderRequest.ortb2; const networkId = getValue(validBidRequests[0].params, 'networkId'); const host = getValue(validBidRequests[0].params, 'host'); const currency = config.getConfig('currency.adServerCurrency') || 'TRY'; const bidderName = validBidRequests[0].bidder; const payload = { - user: { - ua: navigator.userAgent - }, - blacklist: [], + ortb, site: { - page: location.href, - ref: location.origin, + page: bidderRequest.refererInfo.page, + ref: bidderRequest.refererInfo.page, publisher: { - name: location.hostname, + name: bidderRequest.refererInfo.domain, publisherId: networkId } }, @@ -57,17 +89,59 @@ export const spec = { ext: { cur: currency, bidder: bidderName - } + }, + schain: {}, + regs: { + ext: { + } + }, + user: { + ext: {} + }, + at: 1, + tmax: parseInt(tmax) }; - if (!isEmpty(blacklist.badv)) { - payload.blacklist = blacklist.badv; - }; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + const consentStr = (bidderRequest.gdprConsent.consentString) + ? bidderRequest.gdprConsent.consentString.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ''; + const gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + payload.regs.ext.gdpr = gdpr; + payload.regs.ext.consent = consentStr; + } + + if (bidderRequest && bidderRequest.coppa) { + payload.regs.ext.coppa = bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined); + } + + if (bidderRequest && bidderRequest.ortb2?.regs?.gpp) { + payload.regs.ext.gpp = bidderRequest.ortb2?.regs?.gpp; + } + + if (bidderRequest && bidderRequest.ortb2?.regs?.gpp_sid) { + payload.regs.ext.gpp_sid = bidderRequest.ortb2?.regs?.gpp_sid; + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.regs.ext.uspIab = bidderRequest.uspConsent; + } + + if (validBidRequests[0].schain) { + const schain = mapSchain(validBidRequests[0].schain); + if (schain) { + payload.schain = schain; + } + } + + if (validBidRequests[0].userIdAsEids) { + const eids = { eids: validBidRequests[0].userIdAsEids }; + payload.user.ext = { ...payload.user.ext, ...eids }; + } if (payload) { switch (bidderName) { case 'pixad': - SYNC_URL = 'https://static.pixad.com.tr/sync.html'; + SYNC_URL = 'https://static.cdn.pixad.com.tr/sync.html'; break; default: SYNC_URL = 'https://cdn.serve.admatic.com.tr/showad/sync.html'; @@ -78,12 +152,36 @@ export const spec = { } }, - getUserSyncs: function (syncOptions, responses) { - if (syncOptions.iframeEnabled) { - return [{ + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (!hasSynced && syncOptions.iframeEnabled) { + // data is only assigned if params are available to pass to syncEndpoint + let params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = gdprConsent.consentString; + } + } + + if (uspConsent) { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + if (gppConsent?.gppString) { + params['gpp'] = gppConsent.gppString; + params['gpp_sid'] = gppConsent.applicableSections?.toString(); + } + + params = Object.keys(params).length ? `?${formatQS(params)}` : ''; + + hasSynced = true; + return { type: 'iframe', - url: SYNC_URL - }]; + url: SYNC_URL + params + }; } }, @@ -95,41 +193,86 @@ export const spec = { interpretResponse: (response, request) => { const body = response.body; const bidResponses = []; + if (body && body?.data && isArray(body.data)) { body.data.forEach(bid => { - const resbid = { - requestId: bid.id, - cpm: bid.price, - width: bid.width, - height: bid.height, - currency: body.cur || 'TRY', - netRevenue: true, - creativeId: bid.creative_id, - meta: { - advertiserDomains: bid && bid.adomain ? bid.adomain : [] - }, - bidder: bid.bidder, - mediaType: bid.type, - ttl: 60 - }; + const bidRequest = getAssociatedBidRequest(request.data.imp, bid); + if (bidRequest) { + const resbid = { + requestId: bid.id, + cpm: bid.price, + width: bid.width, + height: bid.height, + currency: body.cur || 'TRY', + netRevenue: true, + creativeId: bid.creative_id, + meta: { + model: bid.mime_type, + advertiserDomains: bid && bid.adomain ? bid.adomain : [] + }, + bidder: bid.bidder, + mediaType: bid.type, + ttl: 60 + }; - if (resbid.mediaType === 'video' && isUrl(bid.party_tag)) { - resbid.vastUrl = bid.party_tag; - resbid.vastImpUrl = bid.iurl; - } else if (resbid.mediaType === 'video') { - resbid.vastXml = bid.party_tag; - resbid.vastImpUrl = bid.iurl; - } else if (resbid.mediaType === 'banner') { - resbid.ad = bid.party_tag; - }; + if (resbid.mediaType === 'video' && isUrl(bid.party_tag)) { + resbid.vastUrl = bid.party_tag; + } else if (resbid.mediaType === 'video') { + resbid.vastXml = bid.party_tag; + } else if (resbid.mediaType === 'banner') { + resbid.ad = bid.party_tag; + } else if (resbid.mediaType === 'native') { + resbid.native = interpretNativeAd(bid.party_tag) + }; + + const context = deepAccess(bidRequest, 'mediatype.context'); + if (resbid.mediaType === 'video' && context === 'outstream') { + resbid.renderer = createOutstreamVideoRenderer(bid); + } - bidResponses.push(resbid); + bidResponses.push(resbid); + } }); } return bidResponses; } }; +var hasSynced = false; + +export function resetUserSync() { + hasSynced = false; +} + +/** + * @param {object} schain object set by Publisher + * @returns {object} OpenRTB SupplyChain object + */ +function mapSchain(schain) { + if (!schain) { + return null; + } + if (!validateSchain(schain)) { + logError('AdMatic: required schain params missing'); + return null; + } + return schain; +} + +/** + * @param {object} schain object set by Publisher + * @returns {object} bool + */ +function validateSchain(schain) { + if (!schain.nodes) { + return false; + } + const requiredFields = ['asi', 'sid', 'hp']; + return schain.nodes.every(node => { + return requiredFields.every(field => node[field]); + }); +} + function isUrl(str) { try { URL(str); @@ -139,6 +282,40 @@ function isUrl(str) { } }; +function outstreamRender (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid.adResponse + }); + }); +} + +function createOutstreamVideoRenderer(bid) { + const renderer = Renderer.install({ + id: bid.bidId, + url: RENDERER_URL, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logError('Prebid Error calling setRender on renderer' + err); + } + + return renderer; +} + +function getAssociatedBidRequest(bidRequests, bid) { + for (const request of bidRequests) { + if (request.id === bid.id) { + return request; + } + } + return undefined; +} + function enrichSlotWithFloors(slot, bidRequest) { try { const slotFloors = {}; @@ -156,6 +333,11 @@ function enrichSlotWithFloors(slot, bidRequest) { videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({ size: videoSize, mediaType: VIDEO })); } + if (bidRequest.mediaTypes?.native) { + slotFloors.native = {}; + slotFloors.native['*'] = bidRequest.getFloor({ size: '*', mediaType: NATIVE }); + } + if (Object.keys(slotFloors).length > 0) { if (!slot) { slot = {} @@ -195,6 +377,16 @@ function buildRequestObject(bid) { reqObj.type = 'video'; reqObj.mediatype = bid.mediaTypes.video; } + if (bid.mediaTypes?.native) { + reqObj.type = 'native'; + reqObj.size = [{w: 1, h: 1}]; + reqObj.mediatype = bid.mediaTypes.native; + } + + if (deepAccess(bid, 'ortb2Imp.ext')) { + reqObj.ext = bid.ortb2Imp.ext; + } + reqObj.id = getBidIdParameter('bidId', bid); enrichSlotWithFloors(reqObj, bid); @@ -209,10 +401,11 @@ function getSizes(bid) { function concatSizes(bid) { let playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); let videoSizes = deepAccess(bid, 'mediaTypes.video.sizes'); + let nativeSizes = deepAccess(bid, 'mediaTypes.native.sizes'); let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); if (isArray(bannerSizes) || isArray(playerSize) || isArray(videoSizes)) { - let mediaTypesSizes = [bannerSizes, videoSizes, playerSize]; + let mediaTypesSizes = [bannerSizes, videoSizes, nativeSizes, playerSize]; return mediaTypesSizes .reduce(function(acc, currSize) { if (isArray(currSize)) { @@ -227,6 +420,45 @@ function concatSizes(bid) { } } +function interpretNativeAd(adm) { + const native = JSON.parse(adm).native; + const result = { + clickUrl: encodeURI(native.link.url), + impressionTrackers: native.imptrackers + }; + native.assets.forEach(asset => { + switch (asset.id) { + case OPENRTB.NATIVE.ASSET_ID.TITLE: + result.title = asset.title.text; + break; + case OPENRTB.NATIVE.ASSET_ID.IMAGE: + result.image = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.ICON: + result.icon = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.BODY: + result.body = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.SPONSORED: + result.sponsoredBy = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.CTA: + result.cta = asset.data.value; + break; + } + }); + return result; +} + function _validateId(id) { return (parseInt(id) > 0); } diff --git a/modules/admediaBidAdapter.js b/modules/admediaBidAdapter.js index 42593a36159..5ea3e27b0d9 100644 --- a/modules/admediaBidAdapter.js +++ b/modules/admediaBidAdapter.js @@ -1,6 +1,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'admedia'; const ENDPOINT_URL = 'https://prebid.admedia.com/bidder/'; diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 1006fef631c..f5f0b5bf665 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -14,6 +14,7 @@ const ALIASES = [ {code: 'futureads', endpoint: 'https://ads.futureads.io/prebid.1.2.aspx'}, {code: 'smn', endpoint: 'https://ads.smn.rs/prebid.1.2.aspx'}, {code: 'admixeradx', endpoint: 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'}, + {code: 'admixerwl', endpoint: 'https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx'}, ]; export const spec = { code: BIDDER_CODE, @@ -23,7 +24,9 @@ export const spec = { * Determines whether or not the given bid request is valid. */ isBidRequestValid: function (bid) { - return !!bid.params.zone; + return bid.bidder === 'admixerwl' + ? !!bid.params.clientId && !!bid.params.endpointId + : !!bid.params.zone; }, /** * Make a server request from the list of BidRequests. @@ -73,12 +76,14 @@ export const spec = { validRequest.forEach((bid) => { let imp = {}; Object.keys(bid).forEach(key => imp[key] = bid[key]); + imp.ortb2 && delete imp.ortb2; payload.imps.push(imp); }); + + let urlForRequest = endpointUrl || getEndpointUrl(bidderRequest.bidderCode) return { method: 'POST', - url: - endpointUrl || getEndpointUrl(bidderRequest.bidderCode), + url: bidderRequest.bidderCode === 'admixerwl' ? `${urlForRequest}?client=${payload.imps[0]?.params?.clientId}` : urlForRequest, data: payload, }; }, diff --git a/modules/admixerBidAdapter.md b/modules/admixerBidAdapter.md index 682f5629115..64f8dd64ee4 100644 --- a/modules/admixerBidAdapter.md +++ b/modules/admixerBidAdapter.md @@ -50,3 +50,48 @@ Please use ```admixer``` as the bidder code. }, ]; ``` + +### AdmixerWL Test Parameters +``` + var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + },{ + code: 'mobile-banner-ad-div', + sizes: [[300, 50]], // a mobile size + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + },{ + code: 'video-ad', + sizes: [[300, 50]], + mediaType: 'video', + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + }, + ]; +``` + diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index 0e3a56420a8..cb7248c9537 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -11,6 +11,13 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const NAME = 'admixerId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: NAME}); diff --git a/modules/adnowBidAdapter.js b/modules/adnowBidAdapter.js index f83dbf68a1f..5083f4cc93d 100644 --- a/modules/adnowBidAdapter.js +++ b/modules/adnowBidAdapter.js @@ -5,10 +5,14 @@ import {includes} from '../src/polyfill.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'adnow'; -const ENDPOINT = 'https://n.ads3-adnow.com/a'; +const ENDPOINT = 'https://n.nnowa.com/a'; /** * @typedef {object} CommonBidData + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec * * @property {string} requestId The specific BidRequest which this bid is aimed at. * This should match the BidRequest.bidId which this Bid targets. diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index e44e2b1471a..97419fb94bd 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -14,38 +14,156 @@ const ENDPOINT_URL_EUROPE = 'https://europe.delivery.adnuntius.com/i'; const GVLID = 855; const DEFAULT_VAST_VERSION = 'vast4' const MAXIMUM_DEALS_LIMIT = 5; +const VALID_BID_TYPES = ['netBid', 'grossBid']; +const META_DATA_KEY = 'adn.metaData'; -const checkSegment = function (segment) { - if (isStr(segment)) return segment; - if (segment.id) return segment.id -} +export const misc = { + getUnixTimestamp: function (addDays, asMinutes) { + const multiplication = addDays / (asMinutes ? 1440 : 1); + return Date.now() + (addDays && addDays > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0); + } +}; + +const storageTool = (function () { + const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + let metaInternal; + + const getMetaInternal = function () { + if (!storage.localStorageIsEnabled()) { + return []; + } + + let parsedJson; + try { + parsedJson = JSON.parse(storage.getDataFromLocalStorage(META_DATA_KEY)); + } catch (e) { + return []; + } + + let filteredEntries = parsedJson ? parsedJson.filter((datum) => { + if (datum.key === 'voidAuIds' && Array.isArray(datum.value)) { + return true; + } + return datum.key && datum.value && datum.exp && datum.exp > misc.getUnixTimestamp(); + }) : []; + const voidAuIdsEntry = filteredEntries.find(entry => entry.key === 'voidAuIds'); + if (voidAuIdsEntry) { + const now = misc.getUnixTimestamp(); + voidAuIdsEntry.value = voidAuIdsEntry.value.filter(voidAuId => voidAuId.auId && voidAuId.exp > now); + if (!voidAuIdsEntry.value.length) { + filteredEntries = filteredEntries.filter(entry => entry.key !== 'voidAuIds'); + } + } + return filteredEntries; + }; + + const setMetaInternal = function (apiResponse) { + if (!storage.localStorageIsEnabled()) { + return; + } + + const updateVoidAuIds = function (currentVoidAuIds, auIdsAsString) { + const newAuIds = isStr(auIdsAsString) ? auIdsAsString.split(';') : []; + const notNewExistingAuIds = currentVoidAuIds.filter(auIdObj => { + return newAuIds.indexOf(auIdObj.value) < -1; + }) || []; + const oneDayFromNow = misc.getUnixTimestamp(1); + const apiIdsArray = newAuIds.map(auId => { + return { exp: oneDayFromNow, auId: auId }; + }) || []; + return notNewExistingAuIds.concat(apiIdsArray) || []; + } -const getSegmentsFromOrtb = function (ortb2) { - const userData = deepAccess(ortb2, 'user.data'); - let segments = []; - if (userData) { - userData.forEach(userdat => { - if (userdat.segment) { - segments.push(...userdat.segment.filter(checkSegment).map(checkSegment)); + const metaAsObj = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: { value: entry.value, exp: entry.exp } }), {}); + for (const key in apiResponse) { + if (key !== 'voidAuIds') { + metaAsObj[key] = { + value: apiResponse[key], + exp: misc.getUnixTimestamp(100) + } + } + } + const currentAuIds = updateVoidAuIds(metaAsObj.voidAuIds || [], apiResponse.voidAuIds); + if (currentAuIds.length > 0) { + metaAsObj.voidAuIds = { value: currentAuIds }; + } + const metaDataForSaving = Object.entries(metaAsObj).map((entrySet) => { + if (entrySet[0] === 'voidAuIds') { + return { + key: entrySet[0], + value: entrySet[1].value + }; + } + return { + key: entrySet[0], + value: entrySet[1].value, + exp: entrySet[1].exp } }); + storage.setDataInLocalStorage(META_DATA_KEY, JSON.stringify(metaDataForSaving)); + }; + + const getUsi = function (meta, ortb2, bidderRequest) { + // Fetch user id from parameters. + for (let i = 0; i < (bidderRequest.bids || []).length; i++) { + const bid = bidderRequest.bids[i]; + if (bid.params && bid.params.userId) { + return bid.params.userId; + } + } + if (ortb2 && ortb2.user && ortb2.user.id) { + return ortb2.user.id + } + return (meta && meta.usi) ? meta.usi : false } - return segments -} -const handleMeta = function () { - const storage = getStorageManager({ bidderCode: BIDDER_CODE }) - let adnMeta = null - if (storage.localStorageIsEnabled()) { - adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) + const getSegmentsFromOrtb = function (ortb2) { + const userData = deepAccess(ortb2, 'user.data'); + let segments = []; + if (userData) { + userData.forEach(userdat => { + if (userdat.segment) { + segments.push(...userdat.segment.map((segment) => { + if (isStr(segment)) return segment; + if (isStr(segment.id)) return segment.id; + }).filter((seg) => !!seg)); + } + }); + } + return segments } - return (adnMeta !== null) ? adnMeta.reduce((acc, cur) => { return { ...acc, [cur.key]: cur.value } }, {}) : {} -} -const getUsi = function (meta, ortb2, bidderRequest) { - let usi = (meta !== null && meta.usi) ? meta.usi : false; - if (ortb2 && ortb2.user && ortb2.user.id) { usi = ortb2.user.id } - return usi + return { + refreshStorage: function (bidderRequest) { + const ortb2 = bidderRequest.ortb2 || {}; + metaInternal = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); + metaInternal.usi = getUsi(metaInternal, ortb2, bidderRequest); + if (!metaInternal.usi) { + delete metaInternal.usi; + } + if (metaInternal.voidAuIds) { + metaInternal.voidAuIdsArray = metaInternal.voidAuIds.map((voidAuId) => { + return voidAuId.auId; + }); + } + metaInternal.segments = getSegmentsFromOrtb(ortb2); + }, + saveToStorage: function (serverData) { + setMetaInternal(serverData); + }, + getUrlRelatedData: function () { + const { segments, usi, voidAuIdsArray } = metaInternal; + return { segments, usi, voidAuIdsArray }; + }, + getPayloadRelatedData: function () { + const { segments, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = metaInternal; + return payloadRelatedData; + } + }; +})(); + +const validateBidType = function (bidTypeOption) { + return VALID_BID_TYPES.indexOf(bidTypeOption || '') > -1 ? bidTypeOption : 'bid'; } const AU_ID_REGEX = new RegExp('^[0-9A-Fa-f]{1,20}$'); @@ -62,34 +180,38 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - const networks = {}; - const bidRequests = {}; - const requests = []; - const request = []; - const ortb2 = bidderRequest.ortb2 || {}; - const bidderConfig = config.getConfig(); - - const adnMeta = handleMeta() - const usi = getUsi(adnMeta, ortb2, bidderRequest) - const segments = getSegmentsFromOrtb(ortb2); - const tzo = new Date().getTimezoneOffset(); + const queryParamsAndValues = []; + queryParamsAndValues.push('tzo=' + new Date().getTimezoneOffset()) + queryParamsAndValues.push('format=prebid') const gdprApplies = deepAccess(bidderRequest, 'gdprConsent.gdprApplies'); const consentString = deepAccess(bidderRequest, 'gdprConsent.consentString'); + if (gdprApplies !== undefined) { + const flag = gdprApplies ? '1' : '0' + queryParamsAndValues.push('consentString=' + consentString); + queryParamsAndValues.push('gdpr=' + flag); + } - request.push('tzo=' + tzo) - request.push('format=json') + storageTool.refreshStorage(bidderRequest); + + const urlRelatedMetaData = storageTool.getUrlRelatedData(); + if (urlRelatedMetaData.segments.length > 0) queryParamsAndValues.push('segments=' + urlRelatedMetaData.segments.join(',')); + if (urlRelatedMetaData.usi) queryParamsAndValues.push('userId=' + urlRelatedMetaData.usi); + + const bidderConfig = config.getConfig(); + if (bidderConfig.useCookie === false) queryParamsAndValues.push('noCookies=true'); + if (bidderConfig.maxDeals > 0) queryParamsAndValues.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); + + const bidRequests = {}; + const networks = {}; - if (gdprApplies !== undefined) request.push('consentString=' + consentString); - if (segments.length > 0) request.push('segments=' + segments.join(',')); - if (usi) request.push('userId=' + usi); - if (bidderConfig.useCookie === false) request.push('noCookies=true'); - if (bidderConfig.maxDeals > 0) request.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); for (let i = 0; i < validBidRequests.length; i++) { - const bid = validBidRequests[i] - let network = bid.params.network || 'network'; - const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); - const targeting = bid.params.targeting || {}; + const bid = validBidRequests[i]; + if ((urlRelatedMetaData.voidAuIdsArray && (urlRelatedMetaData.voidAuIdsArray.indexOf(bid.params.auId) > -1 || urlRelatedMetaData.voidAuIdsArray.indexOf(bid.params.auId.padStart(16, '0')) > -1))) { + // This auId is void. Do NOT waste time and energy sending a request to the server + continue; + } + let network = bid.params.network || 'network'; if (bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context !== 'outstream') { network += '_video' } @@ -100,21 +222,31 @@ export const spec = { networks[network] = networks[network] || {}; networks[network].adUnits = networks[network].adUnits || []; if (bidderRequest && bidderRequest.refererInfo) networks[network].context = bidderRequest.refererInfo.page; - if (adnMeta) networks[network].metaData = adnMeta; - const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.bidId, maxDeals: maxDeals } + + const payloadRelatedData = storageTool.getPayloadRelatedData(); + if (Object.keys(payloadRelatedData).length > 0) { + networks[network].metaData = payloadRelatedData; + } + + const targeting = bid.params.targeting || {}; + const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.params.targetId || bid.bidId }; + const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); + if (maxDeals > 0) { + adUnit.maxDeals = maxDeals; + } if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes networks[network].adUnits.push(adUnit); } + const requests = []; const networkKeys = Object.keys(networks) for (let j = 0; j < networkKeys.length; j++) { const network = networkKeys[j]; - const networkRequest = [...request] - if (network.indexOf('_video') > -1) { networkRequest.push('tt=' + DEFAULT_VAST_VERSION) } + if (network.indexOf('_video') > -1) { queryParamsAndValues.push('tt=' + DEFAULT_VAST_VERSION) } const requestURL = gdprApplies ? ENDPOINT_URL_EUROPE : ENDPOINT_URL requests.push({ method: 'POST', - url: requestURL + '?' + networkRequest.join('&'), + url: requestURL + '?' + queryParamsAndValues.join('&'), data: JSON.stringify(networks[network]), bid: bidRequests[network] }); @@ -124,8 +256,20 @@ export const spec = { }, interpretResponse: function (serverResponse, bidRequest) { + if (serverResponse.body.metaData) { + storageTool.saveToStorage(serverResponse.body.metaData); + } const adUnits = serverResponse.body.adUnits; + let validatedBidType = validateBidType(config.getConfig().bidType); + if (bidRequest.bid) { + bidRequest.bid.forEach(b => { + if (b.params && b.params.bidType) { + validatedBidType = validateBidType(b.params.bidType); + } + }); + } + function buildAdResponse(bidderCode, ad, adUnit, dealCount) { const destinationUrls = ad.destinationUrls || {}; const advertiserDomains = []; @@ -135,7 +279,7 @@ export const spec = { const adResponse = { bidderCode: bidderCode, requestId: adUnit.targetId, - cpm: (ad.bid) ? ad.bid.amount * 1000 : 0, + cpm: ad[validatedBidType] ? ad[validatedBidType].amount * 1000 : 0, width: Number(ad.creativeWidth), height: Number(ad.creativeHeight), creativeId: ad.creativeId, diff --git a/modules/adnuntiusRtdProvider.js b/modules/adnuntiusRtdProvider.js index 9234a30aa33..1d5d639aa55 100644 --- a/modules/adnuntiusRtdProvider.js +++ b/modules/adnuntiusRtdProvider.js @@ -5,6 +5,10 @@ import { ajax } from '../src/ajax.js'; import { config as sourceConfig } from '../src/config.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const GVLID = 855; function init(config, userConsent) { diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index c34af4d3d17..9f2810e13df 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -7,8 +7,26 @@ import {config} from '../src/config.js'; import {OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').MediaType} MediaType + * @typedef {import('../src/adapters/bidderFactory.js').Site} Site + * @typedef {import('../src/adapters/bidderFactory.js').Device} Device + * @typedef {import('../src/adapters/bidderFactory.js').User} User + * @typedef {import('../src/adapters/bidderFactory.js').Banner} Banner + * @typedef {import('../src/adapters/bidderFactory.js').Video} Video + * @typedef {import('../src/adapters/bidderFactory.js').AdUnit} AdUnit + * @typedef {import('../src/adapters/bidderFactory.js').Imp} Imp + */ + const BIDDER_CODE = 'adot'; const ADAPTER_VERSION = 'v2.0.0'; +const GVLID = 272; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest'; const REQUIRED_VIDEO_PARAMS = ['mimes', 'protocols']; @@ -635,7 +653,8 @@ export const spec = { isBidRequestValid, buildRequests, interpretResponse, - getFloor + getFloor, + gvlid: GVLID }; registerBidder(spec); diff --git a/modules/adpod.js b/modules/adpod.js index 4ab8e4e5ab9..f6d8309cd9f 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -13,7 +13,6 @@ */ import { - compareOn, deepAccess, generateUUID, groupBy, @@ -28,7 +27,6 @@ import { import { addBidToAuction, AUCTION_IN_PROGRESS, - doCallbacksIfTimedout, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; @@ -212,9 +210,6 @@ function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) { store(bidList, function (error, cacheIds) { if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`); - for (let i = 0; i < bidList.length; i++) { - doCallbacksIfTimedout(auctionInstance, bidList[i]); - } } else { for (let i = 0; i < cacheIds.length; i++) { // when uuid in response is empty string then the key already existed, so this bid wasn't cached @@ -324,7 +319,7 @@ export function checkAdUnitSetupHook(fn, adUnits) { * @param {Object} videoMediaType 'mediaTypes.video' associated to bidResponse * @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory * @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine -*/ + */ function checkBidDuration(videoMediaType, bidResponse) { const buffer = 2; let bidDuration = deepAccess(bidResponse, 'video.durationSeconds'); @@ -595,6 +590,23 @@ function getAdPodAdUnits(codes) { .filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true); } +/** + * This function will create compare function to sort on object property + * @param {string} property + * @returns {function} compare function to be used in sorting + */ +function compareOn(property) { + return function compare(a, b) { + if (a[property] < b[property]) { + return 1; + } + if (a[property] > b[property]) { + return -1; + } + return 0; + } +} + /** * This function removes bids of same category. It will be used when competitive exclusion is enabled. * @param {Array[Object]} bidsReceived diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index 8a953f0d97f..f19cf020ca8 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -1,17 +1,25 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import { logInfo, buildUrl, triggerPixel, parseSizesInput } from '../src/utils.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {buildUrl, logInfo, logMessage, parseSizesInput, triggerPixel} from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ const ADQUERY_GVLID = 902; const ADQUERY_BIDDER_CODE = 'adquery'; const ADQUERY_BIDDER_DOMAIN_PROTOCOL = 'https'; const ADQUERY_BIDDER_DOMAIN = 'bidder.adquery.io'; -const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUERY_BIDDER_DOMAIN + '/prebid/userSync?1=1'; +const ADQUERY_STATIC_DOMAIN_PROTOCOL = 'https'; +const ADQUERY_STATIC_DOMAIN = 'api.adquery.io'; +const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN; const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager({bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { @@ -19,7 +27,7 @@ export const spec = { gvlid: ADQUERY_GVLID, supportedMediaTypes: [BANNER], - /** f + /** * @param {object} bid * @return {boolean} */ @@ -34,10 +42,18 @@ export const spec = { */ buildRequests: (bidRequests, bidderRequest) => { const requests = []; + + let adqueryRequestUrl = buildUrl({ + protocol: ADQUERY_BIDDER_DOMAIN_PROTOCOL, + hostname: ADQUERY_BIDDER_DOMAIN, + pathname: '/prebid/bid', + // search: params + }); + for (let i = 0, len = bidRequests.length; i < len; i++) { const request = { method: 'POST', - url: ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUERY_BIDDER_DOMAIN + '/prebid/bid', + url: adqueryRequestUrl, // ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUERY_BIDDER_DOMAIN + '/prebid/bid', data: buildRequest(bidRequests[i], bidderRequest), options: { withCredentials: false, @@ -55,10 +71,9 @@ export const spec = { * @return {Bid[]} */ interpretResponse: (response, request) => { - logInfo(request); - logInfo(response); + logMessage(request); + logMessage(response); - let qid = null; const res = response && response.body && response.body.data; let bidResponses = []; @@ -87,17 +102,6 @@ export const spec = { bidResponses.push(bidResponse); logInfo('bidResponses', bidResponses); - if (res && res.qid) { - if (storage.getDataFromLocalStorage('qid')) { - qid = storage.getDataFromLocalStorage('qid'); - if (qid && qid.includes('%7B%22')) { - storage.setDataInLocalStorage('qid', res.qid); - } - } else { - storage.setDataInLocalStorage('qid', res.qid); - } - } - return bidResponses; }, @@ -130,8 +134,10 @@ export const spec = { */ onBidWon: (bid) => { logInfo('onBidWon', bid); - const bidString = JSON.stringify(bid); - const encodedBuf = window.btoa(bidString); + let copyOfBid = { ...bid } + delete copyOfBid.ad + const shortBidString = JSON.stringify(copyOfBid); + const encodedBuf = window.btoa(shortBidString); let params = { q: encodedBuf, @@ -170,27 +176,73 @@ export const spec = { }); triggerPixel(adqueryRequestUrl); }, + /** + * Retrieves user synchronization URLs based on provided options and consents. + * + * @param {object} syncOptions - Options for synchronization. + * @param {object[]} serverResponses - Array of server responses. + * @param {object} gdprConsent - GDPR consent object. + * @param {object} uspConsent - USP consent object. + * @returns {object[]} - Array of synchronization URLs. + */ getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncUrl = ADQUERY_USER_SYNC_DOMAIN; - 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}`; - } + logMessage('getUserSyncs', syncOptions, serverResponses, gdprConsent, uspConsent); + let syncData = { + 'gdpr': gdprConsent && gdprConsent.gdprApplies ? 1 : 0, + 'gdpr_consent': gdprConsent && gdprConsent.consentString ? gdprConsent.consentString : '', + 'ccpa_consent': uspConsent && uspConsent.uspConsent ? uspConsent.uspConsent : '', + }; + + if (window.qid) { // only for new users (new qid) + syncData.qid = window.qid; } - if (uspConsent && uspConsent.consentString) { - syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + + let syncUrlObject = { + protocol: ADQUERY_BIDDER_DOMAIN_PROTOCOL, + hostname: ADQUERY_USER_SYNC_DOMAIN, + pathname: '/prebid/userSync', + search: syncData + }; + + if (syncOptions.iframeEnabled) { + syncUrlObject.protocol = ADQUERY_STATIC_DOMAIN_PROTOCOL; + syncUrlObject.hostname = ADQUERY_STATIC_DOMAIN; + syncUrlObject.pathname = '/user-sync-iframe.html'; + + return [{ + type: 'iframe', + url: buildUrl(syncUrlObject) + }]; } + return [{ type: 'image', - url: syncUrl + url: buildUrl(syncUrlObject) }]; } - }; + function buildRequest(validBidRequests, bidderRequest) { let bid = validBidRequests; + logInfo('buildRequest: ', bid); + + let userId = null; + if (window.qid) { + userId = window.qid; + } + + if (bid.userId && bid.userId.qid) { + userId = bid.userId.qid + } + + if (!userId) { + // onetime User ID + const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + userId = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + logMessage('generated onetime User ID: ', userId); + window.qid = userId; + } + let pageUrl = ''; if (bidderRequest && bidderRequest.refererInfo) { pageUrl = bidderRequest.refererInfo.page || ''; @@ -199,11 +251,10 @@ function buildRequest(validBidRequests, bidderRequest) { return { v: '$prebid.version$', placementCode: bid.params.placementId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, + auctionId: null, type: bid.params.type, adUnitCode: bid.adUnitCode, - bidQid: storage.getDataFromLocalStorage('qid') || null, + bidQid: userId, bidId: bid.bidId, bidder: bid.bidder, bidPageUrl: pageUrl, diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index 82df787a2b4..43795b3caba 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -8,9 +8,15 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {isFn, isPlainObject, isStr, logError, logInfo, logMessage} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'qid'; const AU_GVLID = 902; @@ -51,11 +57,7 @@ export const adqueryIdSubmodule = { * @returns {{qid:Object}} */ decode(value) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - return {qid: qid}; - } - return (value && typeof value['qid'] === 'string') ? { 'qid': value['qid'] } : undefined; + return {qid: value} }, /** * performs action to obtain id and return a value in the callback's response argument @@ -64,38 +66,62 @@ export const adqueryIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { + logMessage('adqueryIdSubmodule getId'); + + let qid = storage.getDataFromLocalStorage('qid'); + + if (qid) { + return { + callback: function (callback) { + callback(qid); + } + } + } + if (!isPlainObject(config.params)) { config.params = {}; } - const url = paramOrDefault(config.params.url, + + const url = paramOrDefault( + config.params.url, `https://bidder.adquery.io/prebid/qid`, - config.params.urlArg); + config.params.urlArg + ); const resp = function (callback) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - const responseObj = {qid: qid}; - callback(responseObj); - } else { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - logError(error); - } + let qid = window.qid; + + if (!qid) { + const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + qid = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + + logInfo('adqueryIdSubmodule ID QID GENERTAED:', qid); + } + logInfo('adqueryIdSubmodule ID QID:', qid); + + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); } - callback(responseObj); - }, - error: error => { - logError(`${MODULE_NAME}: ID fetch encountered an error`, error); - callback(); } - }; - ajax(url, callbacks, undefined, {method: 'GET'}); - } + if (responseObj.qid) { + let myQid = responseObj.qid; + storage.setDataInLocalStorage('qid', myQid); + return callback(myQid); + } + callback(); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url + '?qid=' + qid, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; }, diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index cf785a1fc87..68cd859e24e 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -1,8 +1,5 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, @@ -21,7 +18,15 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; @@ -597,6 +602,8 @@ function parseMediaType(rtbBid) { const adType = rtbBid.ad_type; if (adType === VIDEO) { return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; } else { return BANNER; } diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 1af0cffa700..5bce315f572 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,5 +1,5 @@ // ADRIVER BID ADAPTER for Prebid 1.13 -import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; +import {logInfo, getWindowLocation, _each, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index c04ebf48028..2dab76b7862 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -11,6 +11,13 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'adriverId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -35,7 +42,6 @@ export const adriverIdSubmodule = { * performs action to obtain id and return a value in the callback's response argument * @function * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] * @returns {IdResponse|undefined} */ getId(config) { diff --git a/modules/adsinteractiveBidAdapter.js b/modules/adsinteractiveBidAdapter.js index 304b8bcade0..ad6bdfeb299 100644 --- a/modules/adsinteractiveBidAdapter.js +++ b/modules/adsinteractiveBidAdapter.js @@ -6,12 +6,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; const ADSINTERACTIVE_CODE = 'adsinteractive'; -const USER_SYNC_URL_IMAGE = 'https://pb.adsinteractive.com/img'; -const USER_SYNC_URL_IFRAME = 'https://pb.adsinteractive.com/sync'; +const USER_SYNC_URL_IMAGE = 'https://sync.adsinteractive.com/img'; +const USER_SYNC_URL_IFRAME = 'https://sync.adsinteractive.com/sync'; +const GVLID = 1212; export const spec = { code: ADSINTERACTIVE_CODE, supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: (bid) => { return ( diff --git a/modules/adspiritBidAdapter.js b/modules/adspiritBidAdapter.js new file mode 100644 index 00000000000..c39ceca8600 --- /dev/null +++ b/modules/adspiritBidAdapter.js @@ -0,0 +1,124 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; + +const RTB_URL = '/rtb/getbid.php?rtbprovider=prebid'; +const SCRIPT_URL = '/adasync.min.js'; + +export const spec = { + + code: 'adspirit', + aliases: ['twiago'], + supportedMediaTypes: [BANNER, NATIVE], + + isBidRequestValid: function (bid) { + let host = spec.getBidderHost(bid); + if (!host || !bid.params.placementId) { + return false; + } + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + let requests = []; + for (let i = 0; i < validBidRequests.length; i++) { + let bidRequest = validBidRequests[i]; + bidRequest.adspiritConId = spec.genAdConId(bidRequest); + let reqUrl = spec.getBidderHost(bidRequest); + let placementId = utils.getBidIdParameter('placementId', bidRequest.params); + reqUrl = '//' + reqUrl + RTB_URL + '&pid=' + placementId + + '&ref=' + encodeURIComponent(bidderRequest.refererInfo.topmostLocation) + + '&scx=' + (screen.width) + + '&scy=' + (screen.height) + + '&wcx=' + (window.innerWidth || document.documentElement.clientWidth) + + '&wcy=' + (window.innerHeight || document.documentElement.clientHeight) + + '&async=' + bidRequest.adspiritConId + + '&t=' + Math.round(Math.random() * 100000); + + let data = {}; + + if (bidderRequest && bidderRequest.gdprConsent) { + const gdprConsentString = bidderRequest.gdprConsent.consentString; + reqUrl += '&gdpr=' + encodeURIComponent(gdprConsentString); + } + + if (bidRequest.schain && bidderRequest.schain) { + data.schain = bidRequest.schain; + } + + requests.push({ + method: 'GET', + url: reqUrl, + data: data, + bidRequest: bidRequest + }); + } + return requests; + }, + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + let bidObj = bidRequest.bidRequest; + + if (!serverResponse || !serverResponse.body || !bidObj) { + utils.logWarn(`No valid bids from ${spec.code} bidder!`); + return []; + } + + let adData = serverResponse.body; + let cpm = adData.cpm; + + if (!cpm) { + return []; + } + + let host = spec.getBidderHost(bidObj); + + const bidResponse = { + requestId: bidObj.bidId, + cpm: cpm, + width: adData.w, + height: adData.h, + creativeId: bidObj.params.placementId, + currency: 'EUR', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: bidObj && bidObj.adomain ? bidObj.adomain : [] + } + }; + + if ('mediaTypes' in bidObj && 'native' in bidObj.mediaTypes) { + bidResponse.native = { + title: adData.title, + body: adData.body, + cta: adData.cta, + image: { url: adData.image }, + clickUrl: adData.click, + impressionTrackers: [adData.view] + }; + bidResponse.mediaType = NATIVE; + } else { + let adm = '' + adData.adm; + bidResponse.ad = adm; + bidResponse.mediaType = BANNER; + } + + bidResponses.push(bidResponse); + return bidResponses; + }, + getBidderHost: function (bid) { + if (bid.bidder === 'adspirit') { + return utils.getBidIdParameter('host', bid.params); + } + if (bid.bidder === 'twiago') { + return 'a.twiago.com'; + } + return null; + }, + + genAdConId: function (bid) { + return bid.bidder + Math.round(Math.random() * 100000); + } +}; + +registerBidder(spec); diff --git a/modules/adspiritBidAdapter.md b/modules/adspiritBidAdapter.md new file mode 100644 index 00000000000..698ed9b4a0e --- /dev/null +++ b/modules/adspiritBidAdapter.md @@ -0,0 +1,66 @@ + # Overview + + ``` +Module Name: Adspirit Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@adspirit.de + +``` +# Description + +Connects to Adspirit exchange for bids. + +Each adunit with `adspirit` adapter has to have `placementId` and `host`. + + +### Supported Features; + +1. Media Types: Banner & native +2. Multi-format: adUnits +3. Schain module +4. Advertiser domains + + +## Sample Banner Ad Unit + ```javascript + var adUnits = [ + { + code: 'display-div', + + mediaTypes: { + banner: { + sizes: [[300, 250]] //a display size + } + }, + + bids: [ + { + bidder: "adspirit", + params: { + placementId: '7', //Please enter your placementID + host: 'test.adspirit.de' //your host details from Adspirit + } + } + ] + } + ]; + +``` + + +### Privacy Policies + +General Data Protection Regulation(GDPR) is supported by default. + +Complete information on this URL-- https://support.adspirit.de/hc/en-us/categories/115000453312-General + + +### CMP (Consent Management Provider) +CMP stands for Consent Management Provider. In simple terms, this is a service provider that obtains and processes the consent of the user, makes it available to the advertisers and, if necessary, logs it for later control. We recommend using a provider with IAB certification or CMP based on the IAB CMP Framework. A list of IAB CMPs can be found at https://iabeurope.eu/cmp-list/. AdSpirit recommends the use of www.consentmanager.de . + +### List of functions that require consent + +Please visit our page- https://support.adspirit.de/hc/en-us/articles/360014631659-List-of-functions-that-require-consent + + + diff --git a/modules/adstirBidAdapter.js b/modules/adstirBidAdapter.js new file mode 100644 index 00000000000..a0c67ddac7e --- /dev/null +++ b/modules/adstirBidAdapter.js @@ -0,0 +1,92 @@ +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'; + +const BIDDER_CODE = 'adstir'; +const ENDPOINT = 'https://ad.ad-stir.com/prebid' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(utils.isStr(bid.params.appId) && !utils.isEmptyStr(bid.params.appId) && utils.isInteger(bid.params.adSpaceNo)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const sua = utils.deepAccess(validBidRequests[0], 'ortb2.device.sua', null); + + const requests = validBidRequests.map((r) => { + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify({ + appId: r.params.appId, + adSpaceNo: r.params.adSpaceNo, + auctionId: r.auctionId, + transactionId: r.transactionId, + bidId: r.bidId, + mediaTypes: r.mediaTypes, + sizes: r.sizes, + ref: { + page: bidderRequest.refererInfo.page, + tloc: bidderRequest.refererInfo.topmostLocation, + referrer: bidderRequest.refererInfo.ref, + topurl: config.getConfig('pageUrl') ? false : bidderRequest.refererInfo.reachedTop, + }, + sua, + user: utils.deepAccess(r, 'ortb2.user', null), + gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies', false), + usp: (bidderRequest.uspConsent || '1---') !== '1---', + eids: utils.deepAccess(r, 'userIdAsEids', []), + schain: serializeSchain(utils.deepAccess(r, 'schain', null)), + pbVersion: '$prebid.version$', + }), + } + }); + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const seatbid = serverResponse.body.seatbid; + if (!utils.isArray(seatbid)) { + return []; + } + const bids = []; + seatbid.forEach((b) => { + const bid = b.bid || null; + if (!bid) { + return; + } + bids.push(bid); + }); + return bids; + }, +} + +function serializeSchain(schain) { + if (!schain) { + return null; + } + + let serializedSchain = `${schain.ver},${schain.complete}`; + + schain.nodes.map(node => { + serializedSchain += `!${encodeURIComponentForRFC3986(node.asi || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.sid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.hp || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.rid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.name || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.domain || '')}`; + }); + + return serializedSchain; +} + +function encodeURIComponentForRFC3986(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16)}`); +} + +registerBidder(spec); diff --git a/modules/adstirBidAdapter.md b/modules/adstirBidAdapter.md new file mode 100644 index 00000000000..5840697a9b0 --- /dev/null +++ b/modules/adstirBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: adstir Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@ad-stir.com +``` + +# Description + +Module that connects to adstir's demand sources + +Prebid.js version 8.24.0 or above is required to use this adapter. + +# Test Parameters + +``` + var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adstir', + params: { + appId: 'TEST-MEDIA', + adSpaceNo: 1, + } + } + ] + } + ]; +``` diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index 89ba4878acf..a1dec5a420f 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -1,8 +1,9 @@ -import {_map, chunk, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const ENDPOINT = 'https://ghb.console.adtarget.com.tr/v2/auction/'; const BIDDER_CODE = 'adtarget'; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index cab2b8956bc..a95b9ed5652 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -1,9 +1,16 @@ -import {_map, chunk, convertTypes, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ const subdomainSuffixes = ['', 1, 2]; const AUCTION_PATH = '/v2/auction/'; @@ -15,17 +22,12 @@ const HOST_GETTERS = { return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; } }()), - navelix: () => 'ghb.hb.navelix.com', - appaloosa: () => 'ghb.hb.appaloosa.media', - onefiftytwomedia: () => 'ghb.ads.152media.com', - bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', janet: () => 'ghb.bidder.jmgads.com', - pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', - vidcrunchllc: () => 'ghb.platform.vidcrunch.com', '9dotsmedia': () => 'ghb.platform.audiodots.com', - copper6: () => 'ghb.app.copper6.com' + copper6: () => 'ghb.app.copper6.com', + indicue: () => 'ghb.console.indicue.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -42,18 +44,13 @@ export const spec = { code: BIDDER_CODE, gvlid: 410, aliases: [ - 'onefiftytwomedia', - 'appaloosa', - 'bidsxchange', 'streamkey', 'janet', { code: 'selectmedia', gvlid: 775 }, - { code: 'navelix', gvlid: 380 }, - 'pgam', { code: 'ocm', gvlid: 1148 }, - { code: 'vidcrunchllc', gvlid: 1145 }, '9dotsmedia', 'copper6', + 'indicue', ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { @@ -123,7 +120,7 @@ export const spec = { /** * Unpack the response from the server into a list of bids * @param serverResponse - * @param bidderRequest + * @param adapterRequest * @return {Bid[]} An array of bids which were nested inside the server */ interpretResponse: function (serverResponse, { adapterRequest }) { diff --git a/modules/adtelligentIdSystem.js b/modules/adtelligentIdSystem.js index 440ed9ade75..76713f29775 100644 --- a/modules/adtelligentIdSystem.js +++ b/modules/adtelligentIdSystem.js @@ -8,6 +8,13 @@ import * as ajax from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const gvlid = 410; const moduleName = 'adtelligent'; const syncUrl = 'https://idrs.adtelligent.com/get'; diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 1ea5f1a0096..fdc1249ded4 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -1,7 +1,13 @@ -import {deepClone, getAdUnitSizes, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; +import {deepClone, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ export const BIDDER_CODE = 'aduptech'; export const GVLID = 647; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 5930f3adb67..dda88575ff5 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,307 +1,65 @@ // jshint esversion: 6, es3: false, node: true -'use strict'; - -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { convertTypes } from '../libraries/transformParamsUtils/convertTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { - _map, - deepAccess, - deepSetValue, - getDNT, isArray, - isPlainObject, - isStr, - mergeDeep, - parseSizesInput, replaceAuctionPrice, - triggerPixel + triggerPixel, + logMessage, + deepSetValue, + getBidIdParameter } from '../src/utils.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - -const { getConfig } = config; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adxcg'; const SECURE_BID_URL = 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'; -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 DEFAULT_CURRENCY = 'EUR'; +const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; +const DEFAULT_TMAX = 500; +/** + * Adxcg Bid Adapter. + * + */ export const spec = { + code: BIDDER_CODE, - supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], + + aliases: ['mediaopti'], + + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid: (bid) => { + logMessage('adxcg - validating isBidRequestValid'); const params = bid.params || {}; const { adzoneid } = params; return !!(adzoneid); }, - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let app, site; - - const commonFpd = bidderRequest.ortb2 || {}; - let { user } = commonFpd; - - if (typeof getConfig('app') === 'object') { - app = getConfig('app') || {}; - if (commonFpd.app) { - mergeDeep(app, commonFpd.app); - } - } else { - site = getConfig('site') || {}; - if (commonFpd.site) { - mergeDeep(site, commonFpd.site); - } - - if (!site.page) { - site.page = bidderRequest.refererInfo.page; - site.domain = bidderRequest.refererInfo.domain; - } - } - - const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; - device.ua = device.ua || navigator.userAgent; - device.dnt = getDNT() ? 1 : 0; - device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - - const tid = bidderRequest.ortb2?.source?.tid; - const test = setOnAny(validBidRequests, 'params.test'); - const currency = getConfig('currency.adServerCurrency'); - const cur = currency && [ currency ]; - const eids = setOnAny(validBidRequests, 'userIdAsEids'); - const schain = setOnAny(validBidRequests, 'schain'); - - const imp = validBidRequests.map((bid, id) => { - const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' - }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; - const { adzoneid } = bid.params; - - const imp = { - id: id + 1, - tagid: adzoneid, - secure: 1, - bidfloor, - bidfloorcur, - ext: { - } - }; - - const assets = _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); - - if (assets.length) { - imp.native = { - request: JSON.stringify({assets: assets}) - }; - } - - const bannerParams = deepAccess(bid, 'mediaTypes.banner'); - - if (bannerParams && bannerParams.sizes) { - const sizes = parseSizesInput(bannerParams.sizes); - const format = sizes.map(size => { - const [ width, height ] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); - - imp.banner = { - format - }; - } - - const videoParams = deepAccess(bid, 'mediaTypes.video'); - if (videoParams) { - imp.video = videoParams; - } - - return imp; - }); - - const request = { - id: bidderRequest.auctionId, - site, - app, - user, - geo: { utcoffset: new Date().getTimezoneOffset() }, - device, - source: { tid, fd: 1 }, - ext: { - prebid: { - channel: { - name: 'pbjs', - version: '$prebid.version$' - } - } - }, - cur, - imp - }; - - if (test) { - request.is_debug = !!test; - request.test = 1; - } - if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); - } - - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (eids) { - deepSetValue(request, 'user.ext.eids', eids); - } - - if (schain) { - deepSetValue(request, 'source.ext.schain', schain); - } + buildRequests: (bidRequests, bidderRequest) => { + const data = converter.toORTB({ bidRequests, bidderRequest }); return { method: 'POST', url: SECURE_BID_URL, - data: JSON.stringify(request), + data, options: { contentType: 'application/json' }, - bids: validBidRequests + bidderRequest }; }, - interpretResponse: function(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 mediaType = deepAccess(bidResponse, 'ext.crType'); - const result = { - requestId: bid.bidId, - cpm: bidResponse.price, - creativeId: bidResponse.crid, - ttl: bidResponse.ttl ? bidResponse.ttl : 300, - netRevenue: bid.netRevenue === 'net', - currency: cur, - burl: bid.burl || '', - mediaType: mediaType, - width: bidResponse.w, - height: bidResponse.h, - dealId: bidResponse.dealid, - }; - deepSetValue(result, 'meta.mediaType', mediaType); - if (isArray(bidResponse.adomain)) { - deepSetValue(result, 'meta.advertiserDomains', bidResponse.adomain); - } - - if (isPlainObject(bidResponse.ext)) { - if (isStr(bidResponse.ext.mediaType)) { - deepSetValue(result, 'meta.mediaType', mediaType); - } - if (isStr(bidResponse.ext.advertiser_id)) { - deepSetValue(result, 'meta.advertiserId', bidResponse.ext.advertiser_id); - } - if (isStr(bidResponse.ext.advertiser_name)) { - deepSetValue(result, 'meta.advertiserName', bidResponse.ext.advertiser_name); - } - if (isStr(bidResponse.ext.agency_name)) { - deepSetValue(result, 'meta.agencyName', bidResponse.ext.agency_name); - } - } - if (mediaType === BANNER) { - result.ad = bidResponse.adm; - } else if (mediaType === NATIVE) { - result.native = parseNative(bidResponse); - result.width = 0; - result.height = 0; - } else if (mediaType === VIDEO) { - result.vastUrl = bidResponse.nurl; - result.vastXml = bidResponse.adm; - } - - return result; - } - }).filter(Boolean); + interpretResponse: (response, request) => { + if (response.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { const syncs = []; let syncUrl = config.getConfig('adxcg.usersyncUrl'); @@ -323,44 +81,95 @@ export const spec = { } return syncs; }, + onBidWon: (bid) => { // for native requests we put the nurl as an imp tracker, otherwise if the auction takes place on prebid server // the server JS adapter puts the nurl in the adm as a tracking pixel and removes the attribute if (bid.nurl) { triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm)) } + }, + transformBidParams: function (params) { + return convertTypes({ + 'cf': 'string', + 'cp': 'number', + 'ct': 'number', + 'adzoneid': 'string' + }, params); } }; -registerBidder(spec); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: 'EUR' + }, -function parseNative(bid) { - const { assets, link, imptrackers, jstracker } = JSON.parse(bid.adm); - 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 }; + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // tagid + imp.tagid = bidRequest.params.adzoneid.toString(); + // unknown params + const unknownParams = slotUnknownParams(bidRequest); + if (imp.ext || unknownParams) { + imp.ext = Object.assign({}, imp.ext, unknownParams); + } + // battr + if (bidRequest.params.battr) { + ['banner', 'video', 'audio', 'native'].forEach(k => { + if (imp[k]) { + imp[k].battr = bidRequest.params.battr; + } + }); + } + // deals + if (bidRequest.params.deals && isArray(bidRequest.params.deals)) { + imp.pmp = { + private_auction: 0, + deals: bidRequest.params.deals + }; } - }); - return result; -} -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; + imp.secure = Number(window.location.protocol === 'https:'); + + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } - } -} + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + request.tmax = request.tmax || DEFAULT_TMAX; + request.test = config.getConfig('debug') ? 1 : 0; + request.at = 1; + deepSetValue(request, 'ext.prebid.channel.name', 'pbjs'); + deepSetValue(request, 'ext.prebid.channel.version', '$prebid.version$'); + return request; + }, -function flatten(arr) { - return [].concat(...arr); + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; + }, +}); + +/** + * Unknown params are captured and sent on ext + */ +function slotUnknownParams(slot) { + const ext = {}; + const knownParamsMap = {}; + KNOWN_PARAMS.forEach(value => knownParamsMap[value] = 1); + Object.keys(slot.params).forEach(key => { + if (!knownParamsMap[key]) { + ext[key] = slot.params[key]; + } + }); + return Object.keys(ext).length > 0 ? { prebid: ext } : null; } + +registerBidder(spec); diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 4080d9f25cd..ad1c0af039e 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,9 +1,16 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ + const VERSION = '1.0'; const BIDDER_CODE = 'adyoulike'; const DEFAULT_DC = 'hb-api'; @@ -56,13 +63,15 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {bidRequests} - bidRequests.bids[] is an array of AdUnits and bids + * @param {BidRequest} bidRequests is an array of AdUnits and bids + * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); let hasVideo = false; + let eids; const payload = { Version: VERSION, Bids: bidRequests.reduce((accumulator, bidReq) => { @@ -81,6 +90,9 @@ export const spec = { if (bidReq.schain) { accumulator[bidReq.bidId].SChain = bidReq.schain; } + if (!eids && bidReq.userIdAsEids && bidReq.userIdAsEids.length) { + eids = bidReq.userIdAsEids; + } if (mediatype === NATIVE) { let nativeReq = bidReq.mediaTypes.native; if (nativeReq.type === 'image') { @@ -120,9 +132,8 @@ export const spec = { if (bidderRequest.ortb2) { payload.ortb2 = bidderRequest.ortb2; } - - if (deepAccess(bidderRequest, 'userIdAsEids')) { - payload.userId = bidderRequest.userIdAsEids; + if (eids) { + payload.eids = eids; } payload.pbjs_version = '$prebid.version$'; @@ -163,6 +174,50 @@ export const spec = { } }); return bidResponses; + }, + + /** + * List user sync endpoints. + * Legal information have to be added to the request. + * Only iframe syncs are supported. + * + * @param {*} syncOptions Publisher prebid configuration. + * @param {*} serverResponses A successful response from the server. + * @return {syncs[]} An array of syncs that should be executed. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled) { + return []; + } + + let params = ''; + + // GDPR + if (gdprConsent) { + params += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + params += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + params += '&coppa=1'; + } + + // CCPA + if (uspConsent) { + params += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // GPP + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppConsent.gppString); + params += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + + return [{ + type: 'iframe', + url: `https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4${params}` + }]; } } diff --git a/modules/agmaAnalyticsAdapter.js b/modules/agmaAnalyticsAdapter.js new file mode 100644 index 00000000000..f3933cc7625 --- /dev/null +++ b/modules/agmaAnalyticsAdapter.js @@ -0,0 +1,228 @@ +import { ajax } from '../src/ajax.js'; +import { + generateUUID, + logInfo, + logError, + getPerformanceNow, + isEmpty, + isEmptyStr, +} from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { config } from '../src/config.js'; + +const GVLID = 1122; +const ModuleCode = 'agma'; +const analyticsType = 'endpoint'; +const scriptVersion = '1.8.0'; +const batchDelayInMs = 1000; +const agmaURL = 'https://pbc.agma-analytics.de/v1'; +const pageViewId = generateUUID(); + +const { + EVENTS: { AUCTION_INIT }, +} = CONSTANTS; + +// Helper functions +const getScreen = () => { + const w = window; + const d = document; + const e = d.documentElement; + const g = d.getElementsByTagName('body')[0]; + const x = w.innerWidth || e.clientWidth || g.clientWidth; + const y = w.innerHeight || e.clientHeight || g.clientHeight; + return { x, y }; +}; + +const getUserIDs = () => { + try { + return getGlobal().getUserIdsAsEids(); + } catch (e) {} + return []; +}; + +export const getOrtb2Data = (options) => { + let site = null; + let user = null; + + // check if data is provided via config + if (options.ortb2) { + if (options.ortb2.user) { + user = options.ortb2.user; + } + if (options.ortb2.site) { + site = options.ortb2.site; + } + if (site && user) { + return { site, user }; + } + } + try { + const configData = config.getConfig(); + // try to fallback to global config + if (configData.ortb2) { + site = site || configData.ortb2.site; + user = user || configData.ortb2.user; + } + } catch (e) {} + + return { site, user }; +}; + +export const getTiming = () => { + // Timing API V2 + let ttfb = 0; + try { + const entry = performance.getEntriesByType('navigation')[0]; + ttfb = Math.round(entry.responseStart - entry.startTime); + } catch (e) { + // Timing API V1 + try { + const entry = performance.timing; + ttfb = Math.round(entry.responseStart - entry.fetchStart); + } catch (e) { + // Timing API not available + return null; + } + } + const elapsedTime = getPerformanceNow(); + ttfb = ttfb >= 0 && ttfb <= elapsedTime ? ttfb : 0; + return { + ttfb, + elapsedTime, + }; +}; + +export const getPayload = (auctionIds, options) => { + if (!options || !auctionIds || auctionIds.length === 0) { + return false; + } + const consentData = gdprDataHandler.getConsentData(); + let gdprApplies = true; // we assume gdpr applies + let useExtendedPayload = false; + if (consentData) { + gdprApplies = consentData.gdprApplies; + const consents = consentData.vendorData?.vendor?.consents || {}; + useExtendedPayload = consents[GVLID]; + } + const ortb2 = getOrtb2Data(options); + const ri = getRefererInfo() || {}; + + let payload = { + auctionIds: auctionIds, + triggerEvent: options.triggerEvent, + pageViewId, + domain: ri.domain, + gdprApplies, + code: options.code, + ortb2: { site: ortb2.site }, + pageUrl: ri.page, + prebidVersion: '$prebid.version$', + scriptVersion, + debug: options.debug, + timing: getTiming(), + }; + + if (useExtendedPayload) { + const device = config.getConfig('device') || {}; + const { x, y } = getScreen(); + const userIdsAsEids = getUserIDs(); + payload = { + ...payload, + ortb2, + extended: true, + timestamp: Date.now(), + gdprConsentString: consentData.consentString, + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + referrer: ri.topmostLocation, + pageUrl: ri.page, + screenWidth: x, + screenHeight: y, + deviceWidth: device.w || screen.width, + deviceHeight: device.h || screen.height, + userIdsAsEids, + }; + } + return payload; +}; + +const agmaAnalytics = Object.assign(adapter({ analyticsType }), { + auctionIds: [], + timer: null, + track(data) { + const { eventType, args } = data; + if (eventType === this.options.triggerEvent && args && args.auctionId) { + this.auctionIds.push(args.auctionId); + if (this.timer === null) { + this.timer = setTimeout(() => { + this.processBatch(); + }, batchDelayInMs); + } + } + }, + processBatch() { + const currentBatch = [...this.auctionIds]; + const payload = getPayload(currentBatch, this.options); + this.auctionIds = []; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.send(payload); + }, + send(payload) { + if (!payload) { + return; + } + return ajax( + agmaURL, + () => { + logInfo(ModuleCode, 'flushed', payload); + }, + JSON.stringify(payload), + { + contentType: 'text/plain', + method: 'POST', + } + ); + }, +}); + +agmaAnalytics.originEnableAnalytics = agmaAnalytics.enableAnalytics; +agmaAnalytics.enableAnalytics = function (config = {}) { + const { options } = config; + + if (isEmpty(options)) { + logError(ModuleCode, 'Please set options'); + return false; + } + + if (options.site && !options.code) { + logError(ModuleCode, 'Please set `code` - `site` is deprecated'); + options.code = options.site; + } + + if (!options.code || isEmptyStr(options.code)) { + logError(ModuleCode, 'Please set `code` option - agma Analytics is disabled'); + return false; + } + + agmaAnalytics.options = { + triggerEvent: AUCTION_INIT, + ...options, + }; + + agmaAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: agmaAnalytics, + code: ModuleCode, + gvlid: GVLID, +}); + +export default agmaAnalytics; diff --git a/modules/agmaAnalyticsAdapter.md b/modules/agmaAnalyticsAdapter.md new file mode 100644 index 00000000000..30c88fb92ec --- /dev/null +++ b/modules/agmaAnalyticsAdapter.md @@ -0,0 +1,28 @@ +# Overview + Module Name: Agma Analytics + Module Type: Analytics Adapter + Maintainer: [www.agma-mmc.de](https://www.agma-mmc.de) + Technical Support: [info@mllrsohn.com](mailto:info@mllrsohn.com) + +# Description + +Agma Analytics adapter. Please contact [team-internet@agma-mmc.de](mailto:team-internet@agma-mmc.de) for signup and access to [futher documentation](https://docs.agma-analytics.de). + +# Usage + +Add the `agmaAnalyticsAdapter` to your build: + +``` +gulp build --modules=...,agmaAnalyticsAdapter... +``` + +Configure the analytics module: + +```javascript +pbjs.enableAnalytics({ + provider: 'agma', + options: { + code: 'provided-by-agma' // change to the code you received from agma + } +}); +``` diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index 7469f26156b..c6a5cd96fb6 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -1,27 +1,15 @@ -import { - _each, - contains, - deepAccess, - deepSetValue, - getDNT, - isBoolean, - isNumber, - isStr, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, isBoolean, isNumber, isStr, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {ajax} from '../src/ajax.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'aidem'; const BASE_URL = 'https://zero.aidemsrv.com'; const LOCAL_BASE_URL = 'http://127.0.0.1:8787'; -const AVAILABLE_CURRENCIES = ['USD']; -const DEFAULT_CURRENCY = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const REQUIRED_VIDEO_PARAMS = [ 'mimes', 'protocols', 'context' ]; @@ -37,34 +25,63 @@ export const ERROR_CODES = { }; const endpoints = { - request: `${BASE_URL}/bid/request`, - notice: { - win: `${BASE_URL}/notice/win`, - timeout: `${BASE_URL}/notice/timeout`, - error: `${BASE_URL}/notice/error`, - } + request: `${BASE_URL}/prebidjs/ortb/v2.6/bid/request`, + // notice: { + // win: `${BASE_URL}/notice/win`, + // timeout: `${BASE_URL}/notice/timeout`, + // error: `${BASE_URL}/notice/error`, + // } }; -export function setEndPoints(env = null, path = '', mediaType = BANNER) { +export function setEndPoints(env = null, path = '') { switch (env) { case 'local': - endpoints.request = mediaType === BANNER ? `${LOCAL_BASE_URL}${path}/bid/request` : `${LOCAL_BASE_URL}${path}/bid/videorequest`; - endpoints.notice.win = `${LOCAL_BASE_URL}${path}/notice/win`; - endpoints.notice.error = `${LOCAL_BASE_URL}${path}/notice/error`; - endpoints.notice.timeout = `${LOCAL_BASE_URL}${path}/notice/timeout`; + endpoints.request = `${LOCAL_BASE_URL}${path}/prebidjs/ortb/v2.6/bid/request`; break; case 'main': - endpoints.request = mediaType === BANNER ? `${BASE_URL}${path}/bid/request` : `${BASE_URL}${path}/bid/videorequest`; - endpoints.notice.win = `${BASE_URL}${path}/notice/win`; - endpoints.notice.error = `${BASE_URL}${path}/notice/error`; - endpoints.notice.timeout = `${BASE_URL}${path}/notice/timeout`; + endpoints.request = `${BASE_URL}${path}/prebidjs/ortb/v2.6/bid/request`; break; } return endpoints; } config.getConfig('aidem', function (config) { - if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path, config.aidem.mediaType); } + if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path); } +}); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + request(buildRequest, imps, bidderRequest, context) { + logInfo('Building request'); + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'at', 1); + setPrebidRequestEnvironment(request); + deepSetValue(request, 'regs', getRegs()); + deepSetValue(request, 'site.publisher.id', bidderRequest.bids[0].params.publisherId); + deepSetValue(request, 'site.id', bidderRequest.bids[0].params.siteId); + return request; + }, + imp(buildImp, bidRequest, context) { + logInfo('Building imp bidRequest', bidRequest); + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'tagId', bidRequest.params.placementId); + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + const bidResponse = buildBidResponse(bid, context); + logInfo('Building bidResponse'); + logInfo('bid', bid); + logInfo('bidRequest', bidRequest); + logInfo('bidResponse', bidResponse); + if (bidResponse.mediaType === VIDEO) { + deepSetValue(bidResponse, 'vastUrl', bid.adm); + } + return bidResponse; + } }); // AIDEM Custom FN @@ -89,49 +106,6 @@ function recur(obj) { return result; } -// ================================================================================= -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - return 4; - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - case '5g': - return 7; - default: - return 3; - } - default: - return 0; - } -} - -function getDevice() { - const language = navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage; - return { - ua: navigator.userAgent, - dnt: !!getDNT(), - language: language, - connectiontype: getConnectionType(), - screen_width: screen.width, - screen_height: screen.height - }; -} - function getRegs() { let regs = {}; const consentManagement = config.getConfig('consentManagement'); @@ -157,129 +131,6 @@ function getRegs() { return regs; } -function getPageUrl(bidderRequest) { - return bidderRequest?.refererInfo?.page; -} - -function buildWinNotice(bid) { - const params = bid.params[0]; - const app = deepAccess(bid, 'meta.ext.app') - const winNoticeExt = deepAccess(bid, 'meta.ext.win_notice_ext') - return { - publisherId: params.publisherId, - siteId: params.siteId, - placementId: params.placementId, - burl: deepAccess(bid, 'meta.burl'), - cpm: bid.cpm, - currency: bid.currency, - impid: deepAccess(bid, 'meta.impid'), - dsp_id: deepAccess(bid, 'meta.dsp_id'), - adUnitCode: bid.adUnitCode, - // TODO: fix auctionId/transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, - transactionId: bid.transactionId, - ttl: bid.ttl, - requestTimestamp: bid.requestTimestamp, - responseTimestamp: bid.responseTimestamp, - mediatype: bid.mediaType, - environment: app ? 'app' : 'web', - ...app, - ext: winNoticeExt, - }; -} - -function buildErrorNotice(prebidErrorResponse) { - return { - message: `Prebid.js: Server call for ${prebidErrorResponse.bidderCode} failed.`, - url: encodeURIComponent(getPageUrl(prebidErrorResponse)), - auctionId: prebidErrorResponse.auctionId, - bidderRequestId: prebidErrorResponse.bidderRequestId, - metrics: {} - }; -} - -function hasValidFloor(obj) { - if (!obj) return false; - const hasValue = !isNaN(Number(obj.value)); - const hasCurrency = contains(AVAILABLE_CURRENCIES, obj.currency); - return hasValue && hasCurrency; -} - -function getMediaType(bidRequest) { - if ((bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('video')) || bidRequest.params.hasOwnProperty('video')) { return VIDEO; } - return BANNER; -} - -function getPrebidRequestFields(bidderRequest, bidRequests) { - const payload = {}; - // Base Payload Data - deepSetValue(payload, 'id', bidderRequest.bidderRequestId); - // Impressions - setPrebidImpressionObject(bidRequests, payload); - // Device - deepSetValue(payload, 'device', getDevice()); - // Timeout - deepSetValue(payload, 'tmax', bidderRequest.timeout); - // Currency - deepSetValue(payload, 'cur', DEFAULT_CURRENCY); - // Timezone - deepSetValue(payload, 'tz', new Date().getTimezoneOffset()); - // Privacy Regs - deepSetValue(payload, 'regs', getRegs()); - // Site - setPrebidSiteObject(bidderRequest, payload); - // Environment - setPrebidRequestEnvironment(payload); - // AT auction type - deepSetValue(payload, 'at', 1); - - return payload; -} - -function setPrebidImpressionObject(bidRequests, payload) { - payload.imp = []; - _each(bidRequests, function (bidRequest) { - const impressionObject = {}; - // Placement or ad tag used to initiate the auction - deepSetValue(impressionObject, 'id', bidRequest.bidId); - // Transaction id - // TODO: `imp.tid` is not ORTB, is this intentional? - deepSetValue(impressionObject, 'tid', deepAccess(bidRequest, 'ortb2Imp.ext.tid')); - // placement id - deepSetValue(impressionObject, 'tagid', deepAccess(bidRequest, 'params.placementId', null)); - // Publisher id - deepSetValue(payload, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); - // Site id - deepSetValue(payload, 'site.id', deepAccess(bidRequest, 'params.siteId')); - const mediaType = getMediaType(bidRequest); - switch (mediaType) { - case 'banner': - setPrebidImpressionObjectBanner(bidRequest, impressionObject); - break; - case 'video': - setPrebidImpressionObjectVideo(bidRequest, impressionObject); - break; - } - - // Floor (optional) - setPrebidImpressionObjectFloor(bidRequest, impressionObject); - - impressionObject.imp_ext = {}; - - payload.imp.push(impressionObject); - }); -} - -function setPrebidSiteObject(bidderRequest, payload) { - deepSetValue(payload, 'site.domain', deepAccess(bidderRequest, 'refererInfo.domain')); - deepSetValue(payload, 'site.page', deepAccess(bidderRequest, 'refererInfo.page')); - deepSetValue(payload, 'site.referer', deepAccess(bidderRequest, 'refererInfo.ref')); - deepSetValue(payload, 'site.cat', deepAccess(bidderRequest, 'ortb2.site.cat')); - deepSetValue(payload, 'site.sectioncat', deepAccess(bidderRequest, 'ortb2.site.sectioncat')); - deepSetValue(payload, 'site.keywords', deepAccess(bidderRequest, 'ortb2.site.keywords')); - deepSetValue(payload, 'site.site_ext', deepAccess(bidderRequest, 'ortb2.site.ext')); // see https://docs.prebid.org/features/firstPartyData.html -} - function setPrebidRequestEnvironment(payload) { const __navigator = JSON.parse(JSON.stringify(recur(navigator))); delete __navigator.plugins; @@ -296,92 +147,6 @@ function setPrebidRequestEnvironment(payload) { deepSetValue(payload, 'environment.wpar.innerHeight', window.innerHeight); } -function setPrebidImpressionObjectFloor(bidRequest, impressionObject) { - const floor = deepAccess(bidRequest, 'params.floor'); - if (hasValidFloor(floor)) { - deepSetValue(impressionObject, 'floor.value', floor.value); - deepSetValue(impressionObject, 'floor.currency', floor.currency); - } -} - -function setPrebidImpressionObjectBanner(bidRequest, impressionObject) { - deepSetValue(impressionObject, 'mediatype', BANNER); - deepSetValue(impressionObject, 'banner.topframe', 1); - deepSetValue(impressionObject, 'banner.format', []); - _each(bidRequest.mediaTypes.banner.sizes, function (bannerFormat) { - const format = {}; - deepSetValue(format, 'w', bannerFormat[0]); - deepSetValue(format, 'h', bannerFormat[1]); - deepSetValue(format, 'format_ext', {}); - impressionObject.banner.format.push(format); - }); -} - -function setPrebidImpressionObjectVideo(bidRequest, impressionObject) { - deepSetValue(impressionObject, 'mediatype', VIDEO); - deepSetValue(impressionObject, 'video.format', []); - deepSetValue(impressionObject, 'video.mimes', bidRequest.mediaTypes.video.mimes); - deepSetValue(impressionObject, 'video.minDuration', bidRequest.mediaTypes.video.minduration); - deepSetValue(impressionObject, 'video.maxDuration', bidRequest.mediaTypes.video.maxduration); - deepSetValue(impressionObject, 'video.protocols', bidRequest.mediaTypes.video.protocols); - deepSetValue(impressionObject, 'video.context', bidRequest.mediaTypes.video.context); - deepSetValue(impressionObject, 'video.playbackmethod', bidRequest.mediaTypes.video.playbackmethod); - deepSetValue(impressionObject, 'skip', bidRequest.mediaTypes.video.skip); - deepSetValue(impressionObject, 'skipafter', bidRequest.mediaTypes.video.skipafter); - deepSetValue(impressionObject, 'video.pos', bidRequest.mediaTypes.video.pos); - _each(bidRequest.mediaTypes.video.playerSize, function (videoPlayerSize) { - const format = {}; - deepSetValue(format, 'w', videoPlayerSize[0]); - deepSetValue(format, 'h', videoPlayerSize[1]); - deepSetValue(format, 'format_ext', {}); - impressionObject.video.format.push(format); - }); -} - -function getPrebidResponseBidObject(openRTBResponseBidObject) { - const prebidResponseBidObject = {}; - // Common properties - deepSetValue(prebidResponseBidObject, 'requestId', openRTBResponseBidObject.impid); - deepSetValue(prebidResponseBidObject, 'cpm', parseFloat(openRTBResponseBidObject.price)); - deepSetValue(prebidResponseBidObject, 'creativeId', openRTBResponseBidObject.crid); - deepSetValue(prebidResponseBidObject, 'currency', openRTBResponseBidObject.cur ? openRTBResponseBidObject.cur.toUpperCase() : DEFAULT_CURRENCY); - deepSetValue(prebidResponseBidObject, 'width', openRTBResponseBidObject.w); - deepSetValue(prebidResponseBidObject, 'height', openRTBResponseBidObject.h); - deepSetValue(prebidResponseBidObject, 'dealId', openRTBResponseBidObject.dealid); - deepSetValue(prebidResponseBidObject, 'netRevenue', true); - deepSetValue(prebidResponseBidObject, 'ttl', 60000); - - if (openRTBResponseBidObject.mediatype === VIDEO) { - logInfo('bidObject.mediatype == VIDEO'); - deepSetValue(prebidResponseBidObject, 'mediaType', VIDEO); - deepSetValue(prebidResponseBidObject, 'vastUrl', openRTBResponseBidObject.adm); - } else { - logInfo('bidObject.mediatype == BANNER'); - deepSetValue(prebidResponseBidObject, 'mediaType', BANNER); - deepSetValue(prebidResponseBidObject, 'ad', openRTBResponseBidObject.adm); - } - setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject); - return prebidResponseBidObject; -} - -function setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject) { - logInfo('AIDEM Bid Adapter meta', openRTBResponseBidObject); - deepSetValue(prebidResponseBidObject, 'meta.advertiserDomains', deepAccess(openRTBResponseBidObject, 'meta.advertiserDomains')); - deepSetValue(prebidResponseBidObject, 'meta.ext', deepAccess(openRTBResponseBidObject, 'meta.ext')); - if (openRTBResponseBidObject.cat && Array.isArray(openRTBResponseBidObject.cat)) { - const primaryCatId = openRTBResponseBidObject.cat.shift(); - deepSetValue(prebidResponseBidObject, 'meta.primaryCatId', primaryCatId); - deepSetValue(prebidResponseBidObject, 'meta.secondaryCatIds', openRTBResponseBidObject.cat); - } - deepSetValue(prebidResponseBidObject, 'meta.id', openRTBResponseBidObject.id); - deepSetValue(prebidResponseBidObject, 'meta.dsp_id', openRTBResponseBidObject.dsp_id); - deepSetValue(prebidResponseBidObject, 'meta.adid', openRTBResponseBidObject.adid); - deepSetValue(prebidResponseBidObject, 'meta.burl', openRTBResponseBidObject.burl); - deepSetValue(prebidResponseBidObject, 'meta.impid', openRTBResponseBidObject.impid); - deepSetValue(prebidResponseBidObject, 'meta.cat', openRTBResponseBidObject.cat); - deepSetValue(prebidResponseBidObject, 'meta.cid', openRTBResponseBidObject.cid); -} - function hasValidMediaType(bidRequest) { const supported = hasBannerMediaType(bidRequest) || hasVideoMediaType(bidRequest); if (!supported) { @@ -494,48 +259,38 @@ export const spec = { return passesRateLimit(bidRequest); }, - buildRequests: function(validBidRequests, bidderRequest) { - logInfo('validBidRequests: ', validBidRequests); + buildRequests: function(bidRequests, bidderRequest) { + logInfo('bidRequests: ', bidRequests); logInfo('bidderRequest: ', bidderRequest); - const prebidRequest = getPrebidRequestFields(bidderRequest, validBidRequests); - const payloadString = JSON.stringify(prebidRequest); - + const data = converter.toORTB({bidRequests, bidderRequest}); + logInfo('request payload', data); return { method: 'POST', url: endpoints.request, - data: payloadString, + data, options: { withCredentials: true } }; }, - interpretResponse: function (serverResponse) { - const bids = []; - logInfo('serverResponse: ', serverResponse); - _each(serverResponse.body.bid, function (bidObject) { - logInfo('bidObject: ', bidObject); - if (!bidObject.price || !bidObject.adm) { - return; - } - logInfo('CPM OK'); - const bid = getPrebidResponseBidObject(bidObject); - bids.push(bid); - }); - return bids; + interpretResponse: function (serverResponse, request) { + logInfo('serverResponse body: ', serverResponse.body); + logInfo('request data: ', request.data); + const ortbBids = converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + logInfo('ortbBids: ', ortbBids); + return ortbBids; }, onBidWon: function(bid) { // Bidder specific code logInfo('onBidWon bid: ', bid); - const notice = buildWinNotice(bid); - ajax(endpoints.notice.win, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); + ajax(bid.burl); }, - onBidderError: function({ bidderRequest }) { - // Bidder specific code - const notice = buildErrorNotice(bidderRequest); - ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); - }, + // onBidderError: function({ bidderRequest }) { + // const notice = buildErrorNotice(bidderRequest); + // ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); + // }, }; registerBidder(spec); diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 7c6cf1f5de0..079628c88fc 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -11,6 +11,10 @@ import {getStorageManager} from '../src/storageManager.js'; import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; const AG_TCF_ID = 782; @@ -101,7 +105,7 @@ function init(rtdConfig, userConsent) { /** * Real-time data retrieval from AirGrid - * @param {Object} reqBidsConfigObj + * @param {Object} bidConfig * @param {function} onDone * @param {Object} rtdConfig * @param {Object} userConsent diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index ffab41611ef..e02ab920707 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -1,7 +1,12 @@ -import { getBidIdParameter, tryAppendQueryString, createTrackPixelHtml, logError, logWarn, deepAccess } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; +import {createTrackPixelHtml, logError, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER, NATIVE } from '../src/mediaTypes.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ const BidderCode = 'aja'; const URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; @@ -24,7 +29,7 @@ const BannerSizeMap = { export const spec = { code: BidderCode, - supportedMediaTypes: [VIDEO, BANNER, NATIVE], + supportedMediaTypes: [BANNER], /** * Determines whether or not the given bid has all the params needed to make a valid request. @@ -50,32 +55,27 @@ export const spec = { for (let i = 0, len = validBidRequests.length; i < len; i++) { const bidRequest = validBidRequests[i]; + if ( + (bidRequest.mediaTypes?.native || bidRequest.mediaTypes?.video) && + bidRequest.mediaTypes?.banner) { + continue + } + let queryString = ''; const asi = getBidIdParameter('asi', bidRequest.params); queryString = tryAppendQueryString(queryString, 'asi', asi); queryString = tryAppendQueryString(queryString, 'skt', SDKType); + queryString = tryAppendQueryString(queryString, 'gpid', bidRequest.ortb2Imp?.ext?.gpid) queryString = tryAppendQueryString(queryString, 'tid', bidRequest.ortb2Imp?.ext?.tid) + queryString = tryAppendQueryString(queryString, 'cdep', bidRequest.ortb2?.device?.ext?.cdep) queryString = tryAppendQueryString(queryString, 'prebid_id', bidRequest.bidId); queryString = tryAppendQueryString(queryString, 'prebid_ver', '$prebid.version$'); + queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); + queryString = tryAppendQueryString(queryString, 'schain', spec.serializeSupplyChain(bidRequest.schain || [])) - if (pageUrl) { - queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); - } - - const banner = deepAccess(bidRequest, `mediaTypes.${BANNER}`) - if (banner) { - const adFormatIDs = []; - for (const size of banner.sizes || []) { - if (size.length !== 2) { - continue - } - - const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; - if (adFormatID) { - adFormatIDs.push(adFormatID); - } - } + const adFormatIDs = pickAdFormats(bidRequest) + if (adFormatIDs && adFormatIDs.length > 0) { queryString = tryAppendQueryString(queryString, 'ad_format_ids', adFormatIDs.join(',')); } @@ -86,7 +86,7 @@ export const spec = { })); } - const sua = deepAccess(bidRequest, 'ortb2.device.sua'); + const sua = bidRequest.ortb2?.device?.sua if (sua) { queryString = tryAppendQueryString(queryString, 'sua', JSON.stringify(sua)); } @@ -109,9 +109,17 @@ export const spec = { } const ad = bidderResponseBody.ad; + if (AdType.Banner !== ad.ad_type) { + return [] + } + const bannerAd = bidderResponseBody.ad.banner; const bid = { requestId: ad.prebid_id, + mediaType: BANNER, + ad: bannerAd.tag, + width: bannerAd.w, + height: bannerAd.h, cpm: ad.price, creativeId: ad.creative_id, dealId: ad.deal_id, @@ -119,80 +127,16 @@ export const spec = { netRevenue: true, ttl: 300, // 5 minutes meta: { - advertiserDomains: [] + advertiserDomains: bannerAd.adomain, }, } - - if (AdType.Video === ad.ad_type) { - const videoAd = bidderResponseBody.ad.video; - Object.assign(bid, { - vastXml: videoAd.vtag, - width: videoAd.w, - height: videoAd.h, - renderer: newRenderer(bidderResponseBody), - adResponse: bidderResponseBody, - mediaType: VIDEO - }); - - Array.prototype.push.apply(bid.meta.advertiserDomains, videoAd.adomain) - } else if (AdType.Banner === ad.ad_type) { - const bannerAd = bidderResponseBody.ad.banner; - Object.assign(bid, { - width: bannerAd.w, - height: bannerAd.h, - ad: bannerAd.tag, - mediaType: BANNER + try { + bannerAd.imps.forEach(impTracker => { + const tracker = createTrackPixelHtml(impTracker); + bid.ad += tracker; }); - try { - bannerAd.imps.forEach(impTracker => { - const tracker = createTrackPixelHtml(impTracker); - bid.ad += tracker; - }); - } catch (error) { - logError('Error appending tracking pixel', error); - } - - Array.prototype.push.apply(bid.meta.advertiserDomains, bannerAd.adomain) - } else if (AdType.Native === ad.ad_type) { - const nativeAds = ad.native.template_and_ads.ads; - if (nativeAds.length === 0) { - return []; - } - - const nativeAd = nativeAds[0]; - const assets = nativeAd.assets; - - Object.assign(bid, { - mediaType: NATIVE - }); - - bid.native = { - title: assets.title, - body: assets.description, - cta: assets.cta_text, - sponsoredBy: assets.sponsor, - clickUrl: assets.lp_link, - impressionTrackers: nativeAd.imps, - privacyLink: assets.adchoice_url - }; - - if (assets.img_main !== undefined) { - bid.native.image = { - url: assets.img_main, - width: parseInt(assets.img_main_width, 10), - height: parseInt(assets.img_main_height, 10) - }; - } - - if (assets.img_icon !== undefined) { - bid.native.icon = { - url: assets.img_icon, - width: parseInt(assets.img_icon_width, 10), - height: parseInt(assets.img_icon_height, 10) - }; - } - - Array.prototype.push.apply(bid.meta.advertiserDomains, nativeAd.adomain) + } catch (error) { + logError('Error appending tracking pixel', error); } return [bid]; @@ -226,36 +170,50 @@ export const spec = { return syncs; }, -} -function newRenderer(bidderResponse) { - const renderer = Renderer.install({ - id: bidderResponse.ad.prebid_id, - url: bidderResponse.ad.video.purl, - loaded: false, - }); + /** + * Serialize supply chain object + * @param {Object} supplyChain + * @returns {String | undefined} + */ + serializeSupplyChain: function(supplyChain) { + if (!supplyChain || !supplyChain.nodes) return undefined + const { ver, complete, nodes } = supplyChain + return `${ver},${complete}!${spec.serializeSupplyChainNodes(nodes)}` + }, - try { - renderer.setRender(outstreamRender); - } catch (err) { - logWarn('Prebid Error calling setRender on newRenderer', err); + /** + * Serialize each supply chain nodes + * @param {Array} nodes + * @returns {String} + */ + serializeSupplyChainNodes: function(nodes) { + const fields = ['asi', 'sid', 'hp', 'rid', 'name', 'domain'] + return nodes.map((n) => { + return fields.map((f) => { + return encodeURIComponent(n[f] || '').replace(/!/g, '%21') + }).join(',') + }).join('!') } - - return renderer; } -function outstreamRender(bid) { - bid.renderer.push(() => { - window['aja_vast_player'].init({ - vast_tag: bid.adResponse.ad.video.vtag, - ad_unit_code: bid.adUnitCode, // target div id to render video - width: bid.width, - height: bid.height, - progress: bid.adResponse.ad.video.progress, - loop: bid.adResponse.ad.video.loop, - inread: bid.adResponse.ad.video.inread - }); - }); +function pickAdFormats(bidRequest) { + let sizes = bidRequest.sizes || [] + sizes.push(...(bidRequest.mediaTypes?.banner?.sizes || [])) + + const adFormatIDs = []; + for (const size of sizes) { + if (size.length !== 2) { + continue + } + + const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; + if (adFormatID) { + adFormatIDs.push(adFormatID); + } + } + + return [...new Set(adFormatIDs)] } registerBidder(spec); diff --git a/modules/ajaBidAdapter.md b/modules/ajaBidAdapter.md index 66155875f4d..92ffecaeb9f 100644 --- a/modules/ajaBidAdapter.md +++ b/modules/ajaBidAdapter.md @@ -8,7 +8,7 @@ Maintainer: ssp_support@aja-kk.co.jp # Description Connects to Aja exchange for bids. -Aja bid adapter supports Banner and Outstream Video. +Aja bid adapter supports Banner. # Test Parameters ```js @@ -29,64 +29,6 @@ var adUnits = [ asi: 'tk82gbLmg' } }] - }, - // Video outstream adUnit - { - code: 'prebid_video', - mediaTypes: { - video: { - context: 'outstream', - playerSize: [300, 250] - } - }, - bids: [{ - bidder: 'aja', - params: { - asi: '1-KwEG_iR' - } - }] - }, - // Native adUnit - { - code: 'prebid_native', - mediaTypes: { - native: { - image: { - required: true, - sendId: false - }, - title: { - required: true, - sendId: true - }, - sponsoredBy: { - required: false, - sendId: true - }, - clickUrl: { - required: false, - sendId: true - }, - body: { - required: false, - sendId: true - }, - icon: { - required: false, - sendId: false - }, - privacyLink: { - required: true, - sendId: true - }, - } - }, - bids: [{ - bidder: 'aja', - params: { - asi: 'qxueUGliR' - } - }] } ]; ``` diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index f0bb7eb3a6c..0bd53b2a91f 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -12,6 +12,10 @@ import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/ut import { loadExternalScript } from '../src/adloader.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; const MODULE_CODE = 'akamaidap'; @@ -44,9 +48,8 @@ function mergeLazy(target, source) { /** * Add real-time data & merge segments. - * @param {Object} ortb2 destionation object to merge RTD into + * @param {Object} ortb2 destination object to merge RTD into * @param {Object} rtd - * @param {Object} rtdConfig */ export function addRealTimeData(ortb2, rtd) { logInfo('DEBUG(addRealTimeData) - ENTER'); @@ -60,7 +63,7 @@ export function addRealTimeData(ortb2, rtd) { /** * Real-time data retrieval from Audigent - * @param {Object} reqBidsConfigObj + * @param {Object} bidConfig * @param {function} onDone * @param {Object} rtdConfig * @param {Object} userConsent diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index c087b3061a0..d4e7cab8ed1 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,18 +1,20 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, deepClone} from '../src/utils.js'; +import {deepAccess, deepClone, getDNT, generateUUID, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; -import {VIDEO} from '../src/mediaTypes.js'; +import {VIDEO, BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; const BIDDER_CODE = 'alkimi'; +const GVLID = 1169; export const ENDPOINT = 'https://exchange.alkimi-onboarding.com/bid?prebid=true'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { - return !!(bid.params && bid.params.bidFloor && bid.params.token); + return !!(bid.params && bid.params.token); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -28,27 +30,48 @@ export const spec = { bids.push({ token: bidRequest.params.token, - pos: bidRequest.params.pos, + instl: bidRequest.params.instl, + exp: bidRequest.params.exp, bidFloor: getBidFloor(bidRequest, formatTypes), sizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), playerSizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')), impMediaTypes: formatTypes, - adUnitCode: bidRequest.adUnitCode + adUnitCode: bidRequest.adUnitCode, + video: deepAccess(bidRequest, 'mediaTypes.video'), + banner: deepAccess(bidRequest, 'mediaTypes.banner') }) bidIds.push(bidRequest.bidId) }) - const alkimiConfig = config.getConfig('alkimi'); + const alkimiConfig = config.getConfig('alkimi') + const fullPageAuction = bidderRequest.ortb2?.source?.ext?.full_page_auction + const source = fullPageAuction != undefined ? { ext: { full_page_auction: fullPageAuction } } : undefined + const walletID = alkimiConfig && alkimiConfig.walletID + const user = walletID != undefined ? { ext: { walletID: walletID } } : undefined let payload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: bidderRequest.auctionId, + requestId: generateUUID(), signRequest: {bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID}, bidIds, referer: bidderRequest.refererInfo.page, signature: alkimiConfig && alkimiConfig.signature, schain: validBidRequests[0].schain, - cpp: config.getConfig('coppa') ? 1 : 0 + cpp: config.getConfig('coppa') ? 1 : 0, + device: { + dnt: getDNT() ? 1 : 0, + w: screen.width, + h: screen.height + }, + ortb2: { + source, + user, + site: { + keywords: bidderRequest.ortb2?.site?.keywords + }, + at: bidderRequest.ortb2?.at, + bcat: bidderRequest.ortb2?.bcat, + wseat: bidderRequest.ortb2?.wseat + } } if (bidderRequest && bidderRequest.gdprConsent) { @@ -99,7 +122,7 @@ export const spec = { // banner or video if (VIDEO === bid.mediaType) { - bid.vastXml = bid.ad; + bid.vastUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); } bid.meta = {}; @@ -112,21 +135,12 @@ export const spec = { }, onBidWon: function (bid) { - let winUrl; - if (bid.winUrl || bid.vastUrl) { - winUrl = bid.winUrl ? bid.winUrl : bid.vastUrl; - winUrl = winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); - } else if (bid.ad) { - let trackImg = bid.ad.match(/(?!^)/); - bid.ad = bid.ad.replace(trackImg[0], ''); - winUrl = trackImg[0].split('"')[1]; - winUrl = winUrl.replace(/\$%7BAUCTION_PRICE%7D/, bid.cpm); - } else { - return false; + if (BANNER == bid.mediaType && bid.winUrl) { + const winUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); + ajax(winUrl, null); + return true; } - - ajax(winUrl, null); - return true; + return false; } } diff --git a/modules/ampliffyBidAdapter.js b/modules/ampliffyBidAdapter.js new file mode 100644 index 00000000000..bcd28e5bcf1 --- /dev/null +++ b/modules/ampliffyBidAdapter.js @@ -0,0 +1,419 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {logError, logInfo, triggerPixel} from '../src/utils.js'; + +const BIDDER_CODE = 'ampliffy'; +const GVLID = 1258; +const DEFAULT_ENDPOINT = 'bidder.ampliffy.com'; +const TTL = 600; // Time-to-Live - how long (in seconds) Prebid can use this bid. +const LOG_PREFIX = 'AmpliffyBidder: '; + +function isBidRequestValid(bid) { + logInfo(LOG_PREFIX + 'isBidRequestValid: Code: ' + bid.adUnitCode + ': Param' + JSON.stringify(bid.params), bid.adUnitCode); + if (bid.params) { + if (!bid.params.placementId || !bid.params.format) return false; + + if (bid.params.format.toLowerCase() !== 'video' && bid.params.format.toLowerCase() !== 'display' && bid.params.format.toLowerCase() !== 'all') return false; + if (bid.params.format.toLowerCase() === 'video' && !bid.mediaTypes['video']) return false; + if (bid.params.format.toLowerCase() === 'display' && !bid.mediaTypes['banner']) return false; + + if (!bid.params.server || bid.params.server === '') { + const server = bid.params.type + bid.params.region + bid.params.adnetwork; + if (server && server !== '') bid.params.server = server; + else bid.params.server = DEFAULT_ENDPOINT; + } + return true; + } + return false; +} + +function manageConsentArguments(bidderRequest) { + let consent = null; + if (bidderRequest?.gdprConsent) { + consent = { + gdpr: bidderRequest.gdprConsent.gdprApplies ? '1' : '0', + }; + if (bidderRequest.gdprConsent.consentString) { + consent.consent_string = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + consent.addtl_consent = bidderRequest.gdprConsent.addtlConsent; + } + } + return consent; +} + +function buildRequests(validBidRequests, bidderRequest) { + const bidRequests = []; + for (const bidRequest of validBidRequests) { + for (const sizes of bidRequest.sizes) { + let extraParams = mergeParams(getDefaultParams(), bidRequest.params.extraParams); + // Apply GDPR parameters to request. + extraParams = mergeParams(extraParams, manageConsentArguments(bidderRequest)); + const serverURL = getServerURL(bidRequest.params.server, sizes, bidRequest.params.placementId, extraParams); + logInfo(LOG_PREFIX + serverURL, 'requests'); + extraParams.bidId = bidRequest.bidId; + bidRequests.push({ + method: 'GET', + url: serverURL, + data: extraParams, + bidRequest, + }); + } + logInfo(LOG_PREFIX + 'Building request from: ' + bidderRequest.url + ': ' + JSON.stringify(bidRequests), bidRequest.adUnitCode); + } + return bidRequests; +} +export function getDefaultParams() { + return { + ciu_szs: '1x1', + gdfp_req: '1', + env: 'vp', + output: 'xml_vast4', + unviewed_position_start: '1' + }; +} +export function mergeParams(params, extraParams) { + if (extraParams) { + for (const k in extraParams) { + params[k] = extraParams[k]; + } + } + return params; +} +export function paramsToQueryString(params) { + return Object.entries(params).filter(e => typeof e[1] !== 'undefined').map(e => { + if (e[1]) return encodeURIComponent(e[0]) + '=' + encodeURIComponent(e[1]); + else return encodeURIComponent(e[0]); + }).join('&'); +} +const getCacheBuster = () => Math.floor(Math.random() * (9999999999 - 1000000000)); + +// For testing purposes +let currentUrl = null; +export function getCurrentURL() { + if (!currentUrl) currentUrl = top.location.href; + return currentUrl; +} +export function setCurrentURL(url) { + currentUrl = url; +} +const getCurrentURLEncoded = () => encodeURIComponent(getCurrentURL()); +function getServerURL(server, sizes, iu, queryParams) { + const random = getCacheBuster(); + const size = sizes[0] + 'x' + sizes[1]; + let serverURL = '//' + server + '/gampad/ads'; + queryParams.sz = size; + queryParams.iu = iu; + queryParams.url = getCurrentURL(); + queryParams.description_url = getCurrentURL(); + queryParams.correlator = random; + + return serverURL; +} +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + + const bidResponse = {}; + let mediaType = 'video'; + if ( + bidRequest.bidRequest?.mediaTypes && + !bidRequest.bidRequest.mediaTypes['video'] + ) { + mediaType = 'banner'; + } + bidResponse.requestId = bidRequest.bidRequest.bidId; + bidResponse.width = bidRequest.bidRequest?.sizes[0][0]; + bidResponse.height = bidRequest.bidRequest?.sizes[0][1]; + bidResponse.ttl = TTL; + bidResponse.creativeId = 'ampCreativeID134'; + bidResponse.netRevenue = true; + bidResponse.mediaType = mediaType; + bidResponse.meta = { + advertiserDomains: [], + }; + let xmlStr = serverResponse.body; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + const xmlData = parseXML(xml, bidResponse); + logInfo(LOG_PREFIX + 'Response from: ' + bidRequest.url + ': ' + JSON.stringify(xmlData), bidRequest.bidRequest.adUnitCode); + if (xmlData.cpm < 0 || !xmlData.creativeURL || !xmlData.bidUp) { + return []; + } + bidResponse.cpm = xmlData.cpm; + bidResponse.currency = xmlData.currency; + + if (mediaType === 'video') { + logInfo(LOG_PREFIX + xmlData.creativeURL, 'requests'); + bidResponse.vastUrl = xmlData.creativeURL; + } else { + bidResponse.adUrl = xmlData.creativeURL; + } + if (xmlData.trackingUrl) { + bidResponse.vastImpUrl = xmlData.trackingUrl; + bidResponse.trackingUrl = xmlData.trackingUrl; + } + bidResponses.push(bidResponse); + return bidResponses; +} +const replaceMacros = (txt, cpm, bid) => { + const size = bid.width + 'x' + bid.height; + txt = txt.replaceAll('%%CACHEBUSTER%%', getCacheBuster()); + txt = txt.replaceAll('@@CACHEBUSTER@@', getCacheBuster()); + txt = txt.replaceAll('%%REFERER%%', getCurrentURLEncoded()); + txt = txt.replaceAll('@@REFERER@@', getCurrentURLEncoded()); + txt = txt.replaceAll('%%REFERRER_URL_UNESC%%', getCurrentURLEncoded()); + txt = txt.replaceAll('@@REFERRER_URL_UNESC@@', getCurrentURLEncoded()); + txt = txt.replaceAll('%%PRICE_ESC%%', encodePrice(cpm)); + txt = txt.replaceAll('@@PRICE_ESC@@', encodePrice(cpm)); + txt = txt.replaceAll('%%SIZES%%', size); + txt = txt.replaceAll('@@SIZES@@', size); + return txt; +} +const encodePrice = (price) => { + price = parseFloat(price); + const s = 116.54; + const c = 1; + const a = 1; + let encodedPrice = s * Math.log10(price + a) + c; + encodedPrice = Math.min(200, encodedPrice); + encodedPrice = Math.round(Math.max(1, encodedPrice)); + + // Format the encoded price with leading zeros if necessary + const formattedEncodedPrice = encodedPrice.toString().padStart(3, '0'); + + // Build the encoding key + const encodingKey = `H--${formattedEncodedPrice}`; + + return encodeURIComponent(`vch=${encodingKey}`); +}; + +function extractCT(xml) { + let ct = null; + try { + try { + const vastAdTagURI = xml.getElementsByTagName('VASTAdTagURI')[0] + if (vastAdTagURI) { + let url = null; + for (const childNode of vastAdTagURI.childNodes) { + if (childNode.nodeValue.trim().includes('http')) { + url = decodeURIComponent(childNode.nodeValue); + } + } + const urlParams = new URLSearchParams(url); + ct = urlParams.get('ct') + } + } catch (e) { + } + if (!ct) { + const geoExtensions = xml.querySelectorAll('Extension[type="geo"]'); + geoExtensions.forEach((geoExtension) => { + const countryElement = geoExtension.querySelector('Country'); + if (countryElement) { + ct = countryElement.textContent; + } + }); + } + } catch (e) {} + return ct; +} + +function extractCPM(htmlContent, ct, cpm) { + const cpmMapDiv = htmlContent.querySelectorAll('[cpmMap]')[0]; + if (cpmMapDiv) { + let cpmMapJSON = JSON.parse(cpmMapDiv.getAttribute('cpmMap')); + if ((cpmMapJSON)) { + if (cpmMapJSON[ct]) { + cpm = cpmMapJSON[ct]; + } else if (cpmMapJSON['default']) { + cpm = cpmMapJSON['default']; + } + } + } + return cpm; +} + +function extractCurrency(htmlContent, currency) { + const currencyDiv = htmlContent.querySelectorAll('[cpmCurrency]')[0]; + if (currencyDiv) { + const currencyValue = currencyDiv.getAttribute('cpmCurrency'); + if (currencyValue && currencyValue !== '') { + currency = currencyValue; + } + } + return currency; +} + +function extractCreativeURL(htmlContent, ct, cpm, bid) { + let creativeURL = null; + const creativeMap = htmlContent.querySelectorAll('[creativeMap]')[0]; + if (creativeMap) { + const creativeMapString = creativeMap.getAttribute('creativeMap'); + + const creativeMapJSON = JSON.parse(creativeMapString); + let defaultURL = null; + for (const url of Object.keys(creativeMapJSON)) { + const geo = creativeMapJSON[url]; + if (geo.includes(ct)) { + creativeURL = replaceMacros(url, cpm, bid); + } else if (geo.includes('default')) { + defaultURL = url; + } + } + if (!creativeURL && defaultURL) creativeURL = replaceMacros(defaultURL, cpm, bid); + } + return creativeURL; +} + +function extractSyncs(htmlContent) { + let userSyncsJSON = null; + const userSyncs = htmlContent.querySelectorAll('[userSyncs]')[0]; + if (userSyncs) { + const userSyncsString = userSyncs.getAttribute('userSyncs'); + + userSyncsJSON = JSON.parse(userSyncsString); + } + return userSyncsJSON; +} + +function extractTrackingURL(htmlContent, ret) { + const trackingUrlDiv = htmlContent.querySelectorAll('[bidder-tracking-url]')[0]; + if (trackingUrlDiv) { + const trackingUrl = trackingUrlDiv.getAttribute('bidder-tracking-url'); + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'parseXML: trackingUrl: ', trackingUrl) + ret.trackingUrl = trackingUrl; + } +} + +export function parseXML(xml, bid) { + const ret = { cpm: 0.001, currency: 'EUR', creativeURL: null, bidUp: false }; + const ct = extractCT(xml); + if (!ct) return ret; + + try { + if (ct) { + const companion = xml.getElementsByTagName('Companion')[0]; + const htmlResource = companion.getElementsByTagName('HTMLResource')[0]; + const htmlContent = document.createElement('html'); + htmlContent.innerHTML = htmlResource.textContent; + + ret.cpm = extractCPM(htmlContent, ct, ret.cpm); + ret.currency = extractCurrency(htmlContent, ret.currency); + ret.creativeURL = extractCreativeURL(htmlContent, ct, ret.cpm, bid); + extractTrackingURL(htmlContent, ret); + ret.bidUp = isAllowedToBidUp(htmlContent, getCurrentURL()); + ret.userSyncs = extractSyncs(htmlContent); + } + } catch (e) { + // eslint-disable-next-line no-console + logError(LOG_PREFIX + 'Error parsing XML', e); + } + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'parseXML RET:', ret); + + return ret; +} +export function isAllowedToBidUp(html, currentURL) { + currentURL = currentURL.split('?')[0]; // Remove parameters + let allowedToPush = false; + try { + const domainsMap = html.querySelectorAll('[domainMap]')[0]; + if (domainsMap) { + let domains = JSON.parse(domainsMap.getAttribute('domainMap')); + if (domains.domainMap) { + domains = domains.domainMap; + } + domains.forEach((d) => { + if (currentURL.includes(d) || d === 'all' || d === '*') allowedToPush = true; + }) + } else { + allowedToPush = true; + } + if (allowedToPush) { + const excludedURL = html.querySelectorAll('[excludedURLs]')[0]; + if (excludedURL) { + const excludedURLsString = domainsMap.getAttribute('excludedURLs'); + if (excludedURLsString !== '') { + let excluded = JSON.parse(excludedURLsString); + excluded.forEach((d) => { + if (currentURL.includes(d)) allowedToPush = false; + }) + } + } + } + } catch (e) { + // eslint-disable-next-line no-console + logError(LOG_PREFIX + 'isAllowedToBidUp', e); + } + return allowedToPush; +} + +function getSyncData(options, syncs) { + const ret = []; + if (syncs?.length) { + for (const sync of syncs) { + if (sync.type === 'syncImage' && options.pixelEnabled) { + ret.push({url: sync.url, type: 'image'}); + } else if (sync.type === 'syncIframe' && options.iframeEnabled) { + ret.push({url: sync.url, type: 'iframe'}); + } + } + } + return ret; +} + +function getUserSyncs(syncOptions, serverResponses) { + const userSyncs = []; + for (const serverResponse of serverResponses) { + if (serverResponse.body) { + try { + const xmlStr = serverResponse.body; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + const xmlData = parseXML(xml, {}); + if (xmlData.userSyncs) { + userSyncs.push(...getSyncData(syncOptions, xmlData.userSyncs)); + } + } catch (e) {} + } + } + return userSyncs; +} + +function onBidWon(bid) { + logInfo(`${LOG_PREFIX} WON AMPLIFFY`); + if (bid.trackingUrl) { + let url = bid.trackingUrl; + + // Replace macros with URL-encoded bid parameters + Object.keys(bid).forEach(key => { + const macroKey = `%%${key.toUpperCase()}%%`; + const value = encodeURIComponent(JSON.stringify(bid[key])); + url = url.split(macroKey).join(value); + }); + + triggerPixel(url, () => { + logInfo(`${LOG_PREFIX} send data success`); + }, + (e) => { + logError(`${LOG_PREFIX} send data error`, e); + }); + } +} +function onTimeOut() { + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'TIMEOUT'); +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ['ampliffy', 'amp', 'videoffy', 'publiffy'], + supportedMediaTypes: ['video', 'banner'], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeOut, + onBidWon, +}; + +registerBidder(spec); diff --git a/modules/ampliffyBidAdapter.md b/modules/ampliffyBidAdapter.md new file mode 100644 index 00000000000..a425d910582 --- /dev/null +++ b/modules/ampliffyBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +``` +Module Name: Ampliffy Bidder Adapter +Module Type: Bidder Adapter +Maintainer: bidder@ampliffy.com +``` + +# Description + +Connects to Ampliffy Ad server for bids. + +Ampliffy bid adapter supports Video currently, and has initial support for Banner. + +For more information about [Ampliffy](https://www.ampliffy.com/en/), please contact [info@ampliffy.com](info@ampliffy.com). + +# Sample Ad Unit: For Publishers +```javascript +var videoAdUnit = [ +{ + code: 'video1', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [{ + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: '1213213/example/vrutal_/', + format: 'video' + } + }] +}]; +``` + +``` diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index a773ac70559..6e14f65b0c8 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -14,20 +14,31 @@ import { } from '../src/utils.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import { fetch } from '../src/ajax.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.3.3'; +const VERSION = 'pba1.3.4'; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; -const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; +const TRACKING_BASE = 'https://1x1.a-mo.net/'; +const TRACKING_ENDPOINT = TRACKING_BASE + 'hbx/'; +const POST_TRACKING_ENDPOINT = TRACKING_BASE + 'e'; const AMUID_KEY = '__amuidpb'; function getLocation(request) { return parseUrl(request.refererInfo?.topmostLocation || window.location.href); } +function getTimeoutSize(timeoutData) { + if (timeoutData.sizes == null || timeoutData.sizes.length === 0) { + return [0, 0]; + } + + return timeoutData.sizes[0]; +} + const largestSize = (sizes, mediaTypes) => { const allSizes = sizes .concat(deepAccess(mediaTypes, `${BANNER}.sizes`, []) || []) @@ -149,7 +160,9 @@ function convertRequest(bid) { const tid = deepAccess(bid, 'params.tagId'); const au = - bid.params != null && typeof bid.params.adUnitId === 'string' && bid.params.adUnitId !== '' + bid.params != null && + typeof bid.params.adUnitId === 'string' && + bid.params.adUnitId !== '' ? bid.params.adUnitId : bid.adUnitCode; @@ -202,7 +215,10 @@ function isSyncEnabled(syncConfigP, syncType) { return false; } - if (syncConfig.bidders === '*' || (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1)) { + if ( + syncConfig.bidders === '*' || + (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1) + ) { return syncConfig.filter == null || syncConfig.filter === 'include'; } @@ -219,12 +235,17 @@ function getSyncSettings() { d: 0, l: 0, t: 0, - e: true + e: true, }; } - const settings = { d: syncConfig.syncDelay, l: syncConfig.syncsPerBidder, t: 0, e: syncConfig.syncEnabled } - const all = isSyncEnabled(syncConfig.filterSettings, 'all') + const settings = { + d: syncConfig.syncDelay, + l: syncConfig.syncsPerBidder, + t: 0, + e: syncConfig.syncEnabled, + }; + const all = isSyncEnabled(syncConfig.filterSettings, 'all'); if (all) { settings.t = SYNC_IMAGE & SYNC_IFRAME; @@ -256,12 +277,14 @@ function getGpp(bidderRequest) { return bidderRequest.gppConsent; } - return bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' }; + return ( + bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' } + ); } function buildReferrerInfo(bidderRequest) { if (bidderRequest.refererInfo == null) { - return { r: '', t: false, c: '', l: 0, s: [] } + return { r: '', t: false, c: '', l: 0, s: [] }; } const re = bidderRequest.refererInfo; @@ -272,7 +295,7 @@ function buildReferrerInfo(bidderRequest) { l: re.numIframes, s: re.stack, c: re.canonicalUrl, - } + }; } const isTrue = (boolValue) => @@ -358,28 +381,35 @@ export const spec = { return { data: payload, method: 'POST', + browsingTopics: true, url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT), withCredentials: true, }; }, - getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + getUserSyncs( + syncOptions, + serverResponses, + gdprConsent, + uspConsent, + gppConsent + ) { const qp = { gdpr_consent: enc(gdprConsent?.consentString || ''), gdpr: enc(gdprConsent?.gdprApplies ? 1 : 0), us_privacy: enc(uspConsent || ''), gpp: enc(gppConsent?.gppString || ''), - gpp_sid: enc(gppConsent?.applicableSections || '') + gpp_sid: enc(gppConsent?.applicableSections || ''), }; const iframeSync = { url: `https://prebid.a-mo.net/isyn?${formatQS(qp)}`, - type: 'iframe' + type: 'iframe', }; if (serverResponses == null || serverResponses.length === 0) { if (syncOptions.iframeEnabled) { - return [iframeSync] + return [iframeSync]; } return []; @@ -394,7 +424,10 @@ export const spec = { const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image'; if (syncOptions.iframeEnabled || pixelType === 'image') { - hasFrame = hasFrame || (pixelType === 'iframe') || (syncPixel.indexOf('cchain') !== -1) + hasFrame = + hasFrame || + pixelType === 'iframe' || + syncPixel.indexOf('cchain') !== -1; output.push({ url: syncPixel, type: pixelType, @@ -405,7 +438,7 @@ export const spec = { }); if (!hasFrame && output.length < 2) { - output.push(iframeSync) + output.push(iframeSync); } return output; @@ -470,19 +503,58 @@ export const spec = { aud: targetingData.requestId, a: targetingData.adUnitCode, c2: nestedQs(targetingData.adserverTargeting), + cn3: targetingData.timeToRespond, }); }, onTimeout(timeoutData) { - if (timeoutData == null) { + if (timeoutData == null || !timeoutData.length) { return; } - trackEvent('pbto', { - A: timeoutData.bidder, - bid: timeoutData.bidId, - a: timeoutData.adUnitCode, - cn: timeoutData.timeout, + let common = null; + const events = timeoutData.map((timeout) => { + const params = timeout.params || {}; + const size = getTimeoutSize(timeout); + const { domain, page, ref } = + timeout.ortb2 != null && timeout.ortb2.site != null + ? timeout.ortb2.site + : {}; + + if (common == null) { + common = { + do: domain, + u: page, + U: getUIDSafe(), + re: ref, + V: '$prebid.version$', + vg: '$$PREBID_GLOBAL$$', + }; + } + + return { + A: timeout.bidder, + mid: params.tagId, + a: params.adunitId || timeout.adUnitCode, + bid: timeout.bidId, + n: 'g_pbto', + aud: timeout.transactionId, + w: size[0], + h: size[1], + cn: timeout.timeout, + cn2: timeout.bidderRequestsCount, + cn3: timeout.bidderWinsCount, + }; + }); + + const payload = JSON.stringify({ c: common, e: events }); + fetch(POST_TRACKING_ENDPOINT, { + body: payload, + keepalive: true, + withCredentials: true, + method: 'POST' + }).catch((_e) => { + // do nothing; ignore errors }); }, diff --git a/modules/anyclipBidAdapter.js b/modules/anyclipBidAdapter.js new file mode 100644 index 00000000000..cb9103899a4 --- /dev/null +++ b/modules/anyclipBidAdapter.js @@ -0,0 +1,213 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {deepAccess, isArray, isFn, logError, logInfo} from '../src/utils.js'; +import {config} from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + */ + +const BIDDER_CODE = 'anyclip'; +const ENDPOINT_URL = 'https://prebid.anyclip.com'; +const DEFAULT_CURRENCY = 'USD'; +const NET_REVENUE = false; + +/* + * Get the bid floor value from the bidRequest object, either using the getFloor function or by accessing the 'params.floor' property. + * If the bid floor cannot be determined, return 0 as a fallback value. + */ +function getBidFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return deepAccess(bidRequest, 'params.floor', 0); + } + + try { + const bidFloor = bidRequest.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +/** @type {BidderSpec} */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * @param {object} bid + * @return {boolean} + */ + isBidRequestValid: (bid = {}) => { + const bidder = deepAccess(bid, 'bidder'); + const params = deepAccess(bid, 'params', {}); + const mediaTypes = deepAccess(bid, 'mediaTypes', {}); + const banner = deepAccess(mediaTypes, BANNER, {}); + + const isValidBidder = (bidder === BIDDER_CODE); + const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); + const hasSizes = mediaTypes[BANNER] ? isValidSize : false; + const hasRequiredBidParams = Boolean(params.publisherId && params.supplyTagId); + + const isValid = isValidBidder && hasSizes && hasRequiredBidParams; + if (!isValid) { + logError(`Invalid bid request: isValidBidder: ${isValidBidder}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); + } + return isValid; + }, + + /** + * @param {BidRequest[]} validBidRequests + * @param {*} bidderRequest + * @return {ServerRequest} + */ + buildRequests: (validBidRequests, bidderRequest) => { + const bidRequest = validBidRequests[0]; + + let refererInfo; + if (bidderRequest && bidderRequest.refererInfo) { + refererInfo = bidderRequest.refererInfo; + } + + const timeout = bidderRequest.timeout; + const timeoutAdjustment = timeout - ((20 / 100) * timeout); // timeout adjustment - 20% + + if (isPubTagAvailable()) { + // Options + const options = { + publisherId: bidRequest.params.publisherId, + supplyTagId: bidRequest.params.supplyTagId, + url: refererInfo.page, + domain: refererInfo.domain, + prebidTimeout: timeoutAdjustment, + gpid: bidRequest.adUnitCode, + ext: { + transactionId: bidRequest.transactionId + }, + sizes: bidRequest.sizes.map((size) => { + return {width: size[0], height: size[1]} + }) + } + // Floor + const floor = parseFloat(getBidFloor(bidRequest)); + if (!isNaN(floor)) { + options.ext.floor = floor; + } + // Supply Chain (Schain) + if (bidRequest?.schain) { + options.schain = bidRequest.schain + } + // GDPR & Consent (EU) + if (bidderRequest?.gdprConsent) { + options.gdpr = (bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + options.consent = bidderRequest.gdprConsent.consentString; + } + // GPP + if (bidderRequest?.gppConsent?.gppString) { + options.gpp = { + gppVersion: bidderRequest.gppConsent.gppVersion, + sectionList: bidderRequest.gppConsent.sectionList, + applicableSections: bidderRequest.gppConsent.applicableSections, + gppString: bidderRequest.gppConsent.gppString + } + } + // CCPA (US Privacy) + if (bidderRequest?.uspConsent) { + options.usPrivacy = bidderRequest.uspConsent; + } + // COPPA + if (config.getConfig('coppa') === true) { + options.coppa = 1; + } + // Eids + if (bidRequest?.userIdAsEids) { + const eids = bidRequest.userIdAsEids; + if (eids && eids.length) { + options.eids = eids; + } + } + + // Request bids + const requestBidsPromise = window._anyclip.pubTag.requestBids(options); + if (requestBidsPromise !== undefined) { + requestBidsPromise + .then(() => { + logInfo('PubTag requestBids done'); + }) + .catch((err) => { + logError('PubTag requestBids error', err); + }); + } + + // Request + const payload = { + tmax: timeoutAdjustment + } + + return { + method: 'GET', + url: ENDPOINT_URL, + data: payload, + bidRequest + } + } + }, + + /** + * @param {*} serverResponse + * @param {ServerRequest} bidRequest + * @return {Bid[]} + */ + interpretResponse: (serverResponse, { bidRequest }) => { + const bids = []; + + if (bidRequest && isPubTagAvailable()) { + const bidResponse = window._anyclip.pubTag.getBids(bidRequest.transactionId); + if (bidResponse) { + const { adServer } = bidResponse; + if (adServer) { + bids.push({ + requestId: bidRequest.bidId, + creativeId: adServer.bid.creativeId, + cpm: bidResponse.cpm, + width: adServer.bid.width, + height: adServer.bid.height, + currency: adServer.bid.currency || DEFAULT_CURRENCY, + netRevenue: NET_REVENUE, + ttl: adServer.bid.ttl, + ad: adServer.bid.ad, + meta: adServer.bid.meta + }); + } + } + } + + return bids; + }, + + /** + * @param {Bid} bid + */ + onBidWon: (bid) => { + if (isPubTagAvailable()) { + window._anyclip.pubTag.bidWon(bid); + } + } +} + +/** + * @return {boolean} + */ +const isPubTagAvailable = () => { + return !!(window._anyclip && window._anyclip.pubTag); +} + +registerBidder(spec); diff --git a/modules/anyclipBidAdapter.md b/modules/anyclipBidAdapter.md new file mode 100644 index 00000000000..ed67c9f6722 --- /dev/null +++ b/modules/anyclipBidAdapter.md @@ -0,0 +1,52 @@ +# Overview + +``` +Module Name: AnyClip Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@anyclip.com +``` + +# Description + +Connects to AnyClip Marketplace for bids. + +For more information about [AnyClip](https://anyclip.com), please contact [support@anyclip.com](support@anyclip.com). + +AnyClip bid adapter supports Banner currently*. + +Use `anyclip` as bidder. + +# Bid Parameters + +| Key | Required | Example | Description | +|---------------|----------|--------------------------|---------------------------------------| +| `publisherId` | Yes | `'12345'` | The publisher ID provided by AnyClip | +| `supplyTagId` | Yes | `'-mptNo0BycUG4oCDgGrU'` | The supply tag ID provided by AnyClip | +| `floor` | No | `0.5` | Floor price | + + +# Sample Ad Unit: For Publishers +## Sample Banner only Ad Unit +```js +var adUnits = [{ + code: 'adunit1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bids: [{ + bidder: 'anyclip', + params: { + publisherId: '12345', // required, string + supplyTagId: '-mptNo0BycUG4oCDgGrU', // required, string + floor: 0.5 // optional, floor + } + }] +}] +``` + + diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index e1557d9c6d3..dadbdb72e95 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -96,8 +96,8 @@ export const spec = { payload.device = {}; payload.device.ua = navigator.userAgent; - payload.device.height = window.screen.width; - payload.device.width = window.screen.height; + payload.device.height = window.screen.height; + payload.device.width = window.screen.width; payload.device.dnt = _getDoNotTrack(); payload.device.language = navigator.language; @@ -327,7 +327,7 @@ export function validateGeoObject(geo) { * Get bid floor from Price Floors Module * * @param {Object} bid - * @returns {float||null} + * @returns {?number} */ function getBidFloor(bid) { if (!isFn(bid.getFloor)) { diff --git a/modules/appierBidAdapter.js b/modules/appierBidAdapter.js index 12346d15130..cf89aeefffa 100644 --- a/modules/appierBidAdapter.js +++ b/modules/appierBidAdapter.js @@ -2,6 +2,11 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + export const ADAPTER_VERSION = '1.0.0'; const SUPPORTED_AD_TYPES = [BANNER]; @@ -32,7 +37,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {bidRequests[]} - an array of bids + * @param {object} bidRequests - an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 0660f4f4b10..5210d6f7562 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,17 +1,10 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, getUniqueIdentifierStr, - getWindowFromDocument, isArray, isArrayOfNums, isEmpty, @@ -40,14 +33,22 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid'; const VIDEO_TARGETING = ['id', 'minduration', 'maxduration', 'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset']; -const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api', 'startdelay']; +const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api', 'startdelay', 'placement', 'plcmt']; const USER_PARAMS = ['age', 'externalUid', 'external_uid', 'segments', 'gender', 'dnt', 'language']; const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout']; @@ -71,7 +72,12 @@ const VIDEO_MAPPING = { 'mid_roll': 2, 'post_roll': 3, 'outstream': 4, - 'in-banner': 5 + 'in-banner': 5, + 'in-feed': 6, + 'interstitial': 7, + 'accompanying_content_pre_roll': 8, + 'accompanying_content_mid_roll': 9, + 'accompanying_content_post_roll': 10 } }; const NATIVE_MAPPING = { @@ -105,6 +111,7 @@ export const spec = { aliases: [ { code: 'appnexusAst', gvlid: 32 }, { code: 'emxdigital', gvlid: 183 }, + { code: 'emetriq', gvlid: 213 }, { code: 'pagescience', gvlid: 32 }, { code: 'gourmetads', gvlid: 32 }, { code: 'matomy', gvlid: 32 }, @@ -114,6 +121,7 @@ export const spec = { { code: 'beintoo', gvlid: 618 }, { code: 'projectagora', gvlid: 1032 }, { code: 'uol', gvlid: 32 }, + { code: 'adzymic', gvlid: 32 }, ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -349,6 +357,30 @@ export const spec = { } } + if (bidderRequest?.ortb2?.regs?.ext?.dsa) { + const pubDsaObj = bidderRequest.ortb2.regs.ext.dsa; + const dsaObj = {}; + ['dsarequired', 'pubrender', 'datatopub'].forEach((dsaKey) => { + if (isNumber(pubDsaObj[dsaKey])) { + dsaObj[dsaKey] = pubDsaObj[dsaKey]; + } + }); + + if (isArray(pubDsaObj.transparency) && pubDsaObj.transparency.every((v) => isPlainObject(v))) { + const tpData = []; + pubDsaObj.transparency.forEach((tpObj) => { + if (isStr(tpObj.domain) && tpObj.domain != '' && isArray(tpObj.dsaparams) && tpObj.dsaparams.every((v) => isNumber(v))) { + tpData.push(tpObj); + } + }); + if (tpData.length > 0) { + dsaObj.transparency = tpData; + } + } + + if (!isEmpty(dsaObj)) payload.dsa = dsaObj; + } + if (tags[0].publisher_id) { payload.publisher_id = tags[0].publisher_id; } @@ -407,11 +439,7 @@ export const spec = { getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { function checkGppStatus(gppConsent) { - // this is a temporary measure to supress usersync in US-based GPP regions - // this logic will be revised when proper signals (akin to purpose1 from TCF2) can be determined for US GPP - if (gppConsent && Array.isArray(gppConsent.applicableSections)) { - return gppConsent.applicableSections.every(sec => typeof sec === 'number' && sec <= 5); - } + // user sync suppression for adapters is handled in activity controls and not needed in adapters return true; } @@ -588,6 +616,10 @@ function newBid(serverBid, rtbBid, bidderRequest) { bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } + if (rtbBid.dsa) { + bid.meta = Object.assign({}, bid.meta, { dsa: rtbBid.dsa }); + } + // temporary function; may remove at later date if/when adserver fully supports dchain function setupDChain(rtbBid) { let dchain = { @@ -788,7 +820,7 @@ function bidToTag(bid) { tag.keywords = auKeywords; } - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { tag.gpid = gpid; } @@ -890,15 +922,15 @@ function bidToTag(bid) { tag['video_frameworks'] = apiTmp; } break; - case 'startdelay': + case 'plcmt': case 'placement': - const contextKey = 'context'; - if (typeof tag.video[contextKey] !== 'number') { + if (typeof tag.video.context !== 'number') { + const plcmt = videoMediaType['plcmt']; const placement = videoMediaType['placement']; const startdelay = videoMediaType['startdelay']; - const context = getContextFromPlacement(placement) || getContextFromStartDelay(startdelay); - tag.video[contextKey] = VIDEO_MAPPING[contextKey][context]; + const contextVal = getContextFromPlcmt(plcmt, startdelay) || getContextFromPlacement(placement) || getContextFromStartDelay(startdelay); + tag.video.context = VIDEO_MAPPING.context[contextVal]; } break; } @@ -957,8 +989,12 @@ function getContextFromPlacement(ortbPlacement) { if (ortbPlacement === 2) { return 'in-banner'; - } else if (ortbPlacement > 2) { + } else if (ortbPlacement === 3) { return 'outstream'; + } else if (ortbPlacement === 4) { + return 'in-feed'; + } else if (ortbPlacement === 5) { + return 'intersitial'; } } @@ -976,6 +1012,29 @@ function getContextFromStartDelay(ortbStartDelay) { } } +function getContextFromPlcmt(ortbPlcmt, ortbStartDelay) { + if (!ortbPlcmt) { + return; + } + + if (ortbPlcmt === 2) { + if (!ortbStartDelay) { + return; + } + if (ortbStartDelay === 0) { + return 'accompanying_content_pre_roll'; + } else if (ortbStartDelay === -1) { + return 'accompanying_content_mid_roll'; + } else if (ortbStartDelay === -2) { + return 'accompanying_content_post_roll'; + } + } else if (ortbPlcmt === 3) { + return 'interstitial'; + } else if (ortbPlcmt === 4) { + return 'outstream'; + } +} + function hasUserInfo(bid) { return !!bid.params.user; } @@ -1031,7 +1090,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -1057,7 +1116,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration @@ -1142,7 +1201,7 @@ function outstreamRender(bid, doc) { hideSASIframe(bid.adUnitCode); // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], diff --git a/modules/arcspanRtdProvider.js b/modules/arcspanRtdProvider.js index a7ffa059279..8ccf3d160b9 100644 --- a/modules/arcspanRtdProvider.js +++ b/modules/arcspanRtdProvider.js @@ -2,6 +2,10 @@ import { submodule } from '../src/hook.js'; import { mergeDeep } from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** @type {string} */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'arcspan'; diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index e569f04a2a8..a4a6c78566e 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -1,32 +1,20 @@ -import { - _each, - deepAccess, - deepSetValue, - getDNT, - inIframe, - isArray, - isFn, - logWarn, - parseSizesInput, - tryAppendQueryString -} from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { Renderer } from '../src/Renderer.js'; -import { parseDomain } from '../src/refererDetection.js'; +import {deepAccess, deepSetValue} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'aso'; const DEFAULT_SERVER_URL = 'https://srv.aso1.net'; const DEFAULT_SERVER_PATH = '/prebid/bidder'; -const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; -const VERSION = '$prebid.version$_1.1'; +const DEFAULT_CURRENCY = 'USD'; +const VERSION = '$prebid.version$_2.0'; const TTL = 300; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], aliases: [ {code: 'bcmint'}, {code: 'bidgency'} @@ -36,91 +24,30 @@ export const spec = { return !!bid.params && !!bid.params.zone; }, - buildRequests: (validBidRequests, bidderRequest) => { - let serverRequests = []; + buildRequests: (bidRequests, bidderRequest) => { + let requests = []; - _each(validBidRequests, bidRequest => { - const payload = createBasePayload(bidRequest, bidderRequest); - - const bannerParams = deepAccess(bidRequest, 'mediaTypes.banner'); - const videoParams = deepAccess(bidRequest, 'mediaTypes.video'); - - let imp; - - if (bannerParams && videoParams) { - logWarn('Please note, multiple mediaTypes are not supported. The only banner will be used.') - } - - if (bannerParams) { - imp = createBannerImp(bidRequest, bannerParams) - } else if (videoParams) { - imp = createVideoImp(bidRequest, videoParams) - } - - if (imp) { - payload.imp.push(imp); - } else { - return; - } - - serverRequests.push({ + bidRequests.forEach(bid => { + const data = converter.toORTB({bidRequests: [bid], bidderRequest}); + requests.push({ method: 'POST', - url: getEndpoint(bidRequest), - data: payload, + url: getEndpoint(bid), + data, options: { withCredentials: true, crossOrigin: true }, - bidRequest: bidRequest - }); + bidderRequest + }) }); - - return serverRequests; + return requests; }, - interpretResponse: (serverResponse, {bidRequest}) => { - const response = serverResponse && serverResponse.body; - - if (!response) { - return []; - } - - const serverBids = response.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); - const serverBid = serverBids[0]; - - let bids = []; - - const bid = { - requestId: serverBid.impid, - cpm: serverBid.price, - width: serverBid.w, - height: serverBid.h, - ttl: TTL, - creativeId: serverBid.crid, - netRevenue: true, - currency: response.cur, - mediaType: bidRequest.mediaType, - meta: { - mediaType: bidRequest.mediaType, - advertiserDomains: serverBid.adomain ? serverBid.adomain : [] - } - }; - - if (bid.mediaType === BANNER) { - bid.ad = serverBid.adm; - } else if (bid.mediaType === VIDEO) { - bid.vastXml = serverBid.adm; - if (deepAccess(bidRequest, 'mediaTypes.video.context') === 'outstream') { - bid.adResponse = { - content: bid.vastXml, - }; - bid.renderer = createRenderer(bidRequest, OUTSTREAM_RENDERER_URL); - } + interpretResponse: (response, request) => { + if (response.body) { + return converter.fromORTB({response: response.body, request: request.data}).bids; } - - bids.push(bid); - - return bids; + return []; }, getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { @@ -141,16 +68,25 @@ export const spec = { query = tryAppendQueryString(query, 'us_privacy', uspConsent); } - _each(serverResponses, resp => { + if (query.slice(-1) === '&') { + query = query.slice(0, -1); + } + + serverResponses.forEach(resp => { const userSyncs = deepAccess(resp, 'body.ext.user_syncs'); if (!userSyncs) { return; } - _each(userSyncs, us => { + userSyncs.forEach(us => { + let url = us.url; + if (query) { + url = url + (url.indexOf('?') === -1 ? '?' : '&') + query; + } + urls.push({ type: us.type, - url: us.url + (query ? '?' + query : '') + url: url }); }); }); @@ -160,123 +96,50 @@ export const spec = { } }; -function outstreamRender(bid) { - bid.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ - sizes: [bid.width, bid.height], - targetId: bid.adUnitCode, - adResponse: bid.adResponse, - rendererOptions: bid.renderer.getConfig() - }); - }); -} - -function createRenderer(bid, url) { - const renderer = Renderer.install({ - id: bid.bidId, - url: url, - loaded: false, - config: deepAccess(bid, 'renderer.options'), - adUnitCode: bid.adUnitCode - }); - renderer.setRender(outstreamRender); - return renderer; -} - -function getUrlsInfo(bidderRequest) { - const {page, domain, ref} = bidderRequest.refererInfo; - return { - // TODO: do the fallbacks make sense here? - page: page || bidderRequest.refererInfo?.topmostLocation, - referrer: ref || '', - domain: domain || parseDomain(bidderRequest?.refererInfo?.topmostLocation) - } -} - -function getSize(paramSizes) { - const parsedSizes = parseSizesInput(paramSizes); - const sizes = parsedSizes.map(size => { - const [width, height] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return {w, h}; - }); - - return sizes[0] || null; -} - -function getBidFloor(bidRequest, size) { - if (!isFn(bidRequest.getFloor)) { - return null; - } - - const bidFloor = bidRequest.getFloor({ - mediaType: bidRequest.mediaType, - size: size ? [size.w, size.h] : '*' - }); - - if (!isNaN(bidFloor.floor)) { - return bidFloor; - } - - return null; -} - -function createBaseImp(bidRequest, size) { - const imp = { - id: bidRequest.bidId, - tagid: bidRequest.adUnitCode, - secure: 1 - }; - - const bidFloor = getBidFloor(bidRequest, size); - if (bidFloor !== null) { - imp.bidfloor = bidFloor.floor; - imp.bidfloorcur = bidFloor.currency; - } +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: TTL + }, - return imp; -} + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); -function createBannerImp(bidRequest, bannerParams) { - bidRequest.mediaType = BANNER; + imp.tagid = bidRequest.adUnitCode; + imp.secure = Number(window.location.protocol === 'https:'); + return imp; + }, - const size = getSize(bannerParams.sizes); - const imp = createBaseImp(bidRequest, size); + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); - imp.banner = { - w: size.w, - h: size.h, - topframe: inIframe() ? 0 : 1 - } + if (bidderRequest.gdprConsent) { + const consentsIds = getConsentsIds(bidderRequest.gdprConsent); + if (consentsIds) { + deepSetValue(request, 'user.ext.consents', consentsIds); + } + } - return imp; -} + if (!request.cur) { + request.cur = [DEFAULT_CURRENCY]; + } -function createVideoImp(bidRequest, videoParams) { - bidRequest.mediaType = VIDEO; - const size = getSize(videoParams.playerSize); - const imp = createBaseImp(bidRequest, size); + return request; + }, - imp.video = { - mimes: videoParams.mimes, - minduration: videoParams.minduration, - startdelay: videoParams.startdelay, - linearity: videoParams.linearity, - maxduration: videoParams.maxduration, - skip: videoParams.skip, - protocols: videoParams.protocols, - skipmin: videoParams.skipmin, - api: videoParams.api - } + bidResponse(buildBidResponse, bid, context) { + context.mediaType = deepAccess(bid, 'ext.prebid.type'); + return buildBidResponse(bid, context); + }, - if (size) { - imp.video.w = size.w; - imp.video.h = size.h; + overrides: { + request: { + // We don't need extra data + gdprAddtlConsent(setAddtlConsent, ortbRequest, bidderRequest) { + } + } } - - return imp; -} +}); function getEndpoint(bidRequest) { const serverUrl = bidRequest.params.server || DEFAULT_SERVER_URL; @@ -287,7 +150,7 @@ function getConsentsIds(gdprConsent) { const consents = deepAccess(gdprConsent, 'vendorData.purpose.consents', []); let consentsIds = []; - Object.keys(consents).forEach(function (key) { + Object.keys(consents).forEach(key => { if (consents[key] === true) { consentsIds.push(key); } @@ -296,61 +159,4 @@ function getConsentsIds(gdprConsent) { return consentsIds.join(','); } -function createBasePayload(bidRequest, bidderRequest) { - const urlsInfo = getUrlsInfo(bidderRequest); - - const payload = { - id: bidRequest.bidId, - at: 1, - tmax: bidderRequest.timeout, - site: { - id: urlsInfo.domain, - domain: urlsInfo.domain, - page: urlsInfo.page, - ref: urlsInfo.referrer - }, - device: { - dnt: getDNT() ? 1 : 0, - h: window.innerHeight, - w: window.innerWidth, - }, - imp: [], - ext: {}, - user: {} - }; - - if (bidRequest.params.attr) { - deepSetValue(payload, 'site.ext.attr', bidRequest.params.attr); - } - - if (bidderRequest.gdprConsent) { - deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - const consentsIds = getConsentsIds(bidderRequest.gdprConsent); - if (consentsIds) { - deepSetValue(payload, 'user.ext.consents', consentsIds); - } - deepSetValue(payload, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); - } - - if (bidderRequest.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (config.getConfig('coppa')) { - deepSetValue(payload, 'regs.coppa', 1); - } - - const eids = deepAccess(bidRequest, 'userIdAsEids'); - if (eids && eids.length) { - deepSetValue(payload, 'user.ext.eids', eids); - } - - const schainData = deepAccess(bidRequest, 'schain.nodes'); - if (isArray(schainData) && schainData.length > 0) { - deepSetValue(payload, 'source.ext.schain', bidRequest.schain); - } - - return payload; -} - registerBidder(spec); diff --git a/modules/asoBidAdapter.md b/modules/asoBidAdapter.md index f187389c5b5..ebf5cfd4614 100644 --- a/modules/asoBidAdapter.md +++ b/modules/asoBidAdapter.md @@ -17,7 +17,6 @@ For more information, please visit [Adserver.Online](https://adserver.online). | Name | Scope | Description | Example | Type | |-----------|----------|-------------------------|------------------------|------------| | `zone` | required | Zone ID | `73815` | `Integer` | -| `attr` | optional | Custom targeting params | `{foo: ["a", "b"]}` | `Object` | | `server` | optional | Custom bidder endpoint | `https://endpoint.url` | `String` | # Test parameters for banner @@ -49,6 +48,9 @@ var videoAdUnit = [ code: 'video1', mediaTypes: { video: { + mimes: [ + "video/mp4" + ], playerSize: [[640, 480]], context: 'instream' // or 'outstream' } @@ -69,7 +71,6 @@ The Adserver.Online Bid Adapter expects Prebid Cache (for video) to be enabled. ``` pbjs.setConfig({ - usePrebidCache: true, cache: { url: 'https://prebid.adnxs.com/pbc/v1/cache' } diff --git a/modules/asteriobidAnalyticsAdapter.js b/modules/asteriobidAnalyticsAdapter.js new file mode 100644 index 00000000000..516a3a65667 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.js @@ -0,0 +1,336 @@ +import { generateUUID, getParameterByName, logError, logInfo, parseUrl } from '../src/utils.js' +import { ajaxBuilder } from '../src/ajax.js' +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import { getStorageManager } from '../src/storageManager.js' +import CONSTANTS from '../src/constants.json' +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js' +import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * asteriobidAnalyticsAdapter.js - analytics adapter for AsterioBid + */ +export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'asteriobid' }) +const DEFAULT_EVENT_URL = 'https://endpt.asteriobid.com/endpoint' +const analyticsType = 'endpoint' +const analyticsName = 'AsterioBid Analytics' +const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] +const _VERSION = 1 + +let ajax = ajaxBuilder(20000) +let initOptions +let auctionStarts = {} +let auctionTimeouts = {} +let sampling +let pageViewId +let flushInterval +let eventQueue = [] +let asteriobidAnalyticsEnabled = false + +let asteriobidAnalytics = Object.assign(adapter({ url: DEFAULT_EVENT_URL, analyticsType }), { + track({ eventType, args }) { + handleEvent(eventType, args) + } +}) + +asteriobidAnalytics.originEnableAnalytics = asteriobidAnalytics.enableAnalytics +asteriobidAnalytics.enableAnalytics = function (config) { + initOptions = config.options || {} + + pageViewId = initOptions.pageViewId || generateUUID() + sampling = initOptions.sampling || 1 + + if (Math.floor(Math.random() * sampling) === 0) { + asteriobidAnalyticsEnabled = true + flushInterval = setInterval(flush, 1000) + } else { + logInfo(`${analyticsName} isn't enabled because of sampling`) + } + + asteriobidAnalytics.originEnableAnalytics(config) +} + +asteriobidAnalytics.originDisableAnalytics = asteriobidAnalytics.disableAnalytics +asteriobidAnalytics.disableAnalytics = function () { + if (!asteriobidAnalyticsEnabled) { + return + } + flush() + clearInterval(flushInterval) + asteriobidAnalytics.originDisableAnalytics() +} + +function collectUtmTagData() { + let newUtm = false + let pmUtmTags = {} + try { + utmTags.forEach(function (utmKey) { + let utmValue = getParameterByName(utmKey) + if (utmValue !== '') { + newUtm = true + } + pmUtmTags[utmKey] = utmValue + }) + if (newUtm === false) { + utmTags.forEach(function (utmKey) { + let itemValue = storage.getDataFromLocalStorage(`pm_${utmKey}`) + if (itemValue && itemValue.length !== 0) { + pmUtmTags[utmKey] = itemValue + } + }) + } else { + utmTags.forEach(function (utmKey) { + storage.setDataInLocalStorage(`pm_${utmKey}`, pmUtmTags[utmKey]) + }) + } + } catch (e) { + logError(`${analyticsName} Error`, e) + pmUtmTags['error_utm'] = 1 + } + return pmUtmTags +} + +function collectPageInfo() { + const pageInfo = { + domain: window.location.hostname, + } + if (document.referrer) { + pageInfo.referrerDomain = parseUrl(document.referrer).hostname + } + + const refererInfo = getRefererInfo() + pageInfo.page = refererInfo.page + pageInfo.ref = refererInfo.ref + + return pageInfo +} + +function flush() { + if (!asteriobidAnalyticsEnabled) { + return + } + + if (eventQueue.length > 0) { + const data = { + pageViewId: pageViewId, + ver: _VERSION, + bundleId: initOptions.bundleId, + events: eventQueue, + utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), + sampling: sampling + } + eventQueue = [] + + if ('version' in initOptions) { + data.version = initOptions.version + } + if ('tcf_compliant' in initOptions) { + data.tcf_compliant = initOptions.tcf_compliant + } + if ('adUnitDict' in initOptions) { + data.adUnitDict = initOptions.adUnitDict; + } + if ('customParam' in initOptions) { + data.customParam = initOptions.customParam; + } + + const url = initOptions.url ? initOptions.url : DEFAULT_EVENT_URL + ajax( + url, + () => logInfo(`${analyticsName} sent events batch`), + _VERSION + ':' + JSON.stringify(data), + { + contentType: 'text/plain', + method: 'POST', + withCredentials: true + } + ) + } +} + +function trimAdUnit(adUnit) { + if (!adUnit) return adUnit + const res = {} + res.code = adUnit.code + res.sizes = adUnit.sizes + return res +} + +function trimBid(bid) { + if (!bid) return bid + const res = {} + res.auctionId = bid.auctionId + res.bidder = bid.bidder + res.bidderRequestId = bid.bidderRequestId + res.bidId = bid.bidId + res.crumbs = bid.crumbs + res.cpm = bid.cpm + res.currency = bid.currency + res.mediaTypes = bid.mediaTypes + res.sizes = bid.sizes + res.transactionId = bid.transactionId + res.adUnitCode = bid.adUnitCode + res.bidRequestsCount = bid.bidRequestsCount + res.serverResponseTimeMs = bid.serverResponseTimeMs + return res +} + +function trimBidderRequest(bidderRequest) { + if (!bidderRequest) return bidderRequest + const res = {} + res.auctionId = bidderRequest.auctionId + res.auctionStart = bidderRequest.auctionStart + res.bidderRequestId = bidderRequest.bidderRequestId + res.bidderCode = bidderRequest.bidderCode + res.bids = bidderRequest.bids && bidderRequest.bids.map(trimBid) + return res +} + +function handleEvent(eventType, eventArgs) { + if (!asteriobidAnalyticsEnabled) { + return + } + + try { + eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {} + } catch (e) { + // keep eventArgs as is + } + + const pmEvent = {} + pmEvent.timestamp = eventArgs.timestamp || Date.now() + pmEvent.eventType = eventType + + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeout = eventArgs.timeout + pmEvent.adUnits = eventArgs.adUnits && eventArgs.adUnits.map(trimAdUnit) + pmEvent.bidderRequests = eventArgs.bidderRequests && eventArgs.bidderRequests.map(trimBidderRequest) + auctionStarts[pmEvent.auctionId] = pmEvent.timestamp + auctionTimeouts[pmEvent.auctionId] = pmEvent.timeout + break + } + case CONSTANTS.EVENTS.AUCTION_END: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.end = eventArgs.end + pmEvent.start = eventArgs.start + pmEvent.adUnitCodes = eventArgs.adUnitCodes + pmEvent.bidsReceived = eventArgs.bidsReceived && eventArgs.bidsReceived.map(trimBid) + pmEvent.start = auctionStarts[pmEvent.auctionId] + pmEvent.end = Date.now() + break + } + case CONSTANTS.EVENTS.BID_ADJUSTMENT: { + break + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + pmEvent.bidders = eventArgs && eventArgs.map ? eventArgs.map(trimBid) : eventArgs + pmEvent.duration = auctionTimeouts[pmEvent.auctionId] + break + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.timeout = eventArgs.timeout + break + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.width = eventArgs.width + pmEvent.height = eventArgs.height + pmEvent.adId = eventArgs.adId + pmEvent.mediaType = eventArgs.mediaType + pmEvent.cpm = eventArgs.cpm + pmEvent.currency = eventArgs.currency + pmEvent.requestId = eventArgs.requestId + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeToRespond = eventArgs.timeToRespond + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.size = eventArgs.size + pmEvent.adserverTargeting = eventArgs.adserverTargeting + break + } + case CONSTANTS.EVENTS.BID_WON: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.adId = eventArgs.adId + pmEvent.adserverTargeting = eventArgs.adserverTargeting + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.height = eventArgs.height + pmEvent.mediaType = eventArgs.mediaType + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.cpm = eventArgs.cpm + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.size = eventArgs.size + pmEvent.width = eventArgs.width + pmEvent.currency = eventArgs.currency + pmEvent.bidder = eventArgs.bidder + break + } + case CONSTANTS.EVENTS.BIDDER_DONE: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.timeout = eventArgs.timeout + pmEvent.tid = eventArgs.tid + pmEvent.src = eventArgs.src + break + } + case CONSTANTS.EVENTS.SET_TARGETING: { + break + } + case CONSTANTS.EVENTS.REQUEST_BIDS: { + break + } + case CONSTANTS.EVENTS.ADD_AD_UNITS: { + break + } + case CONSTANTS.EVENTS.AD_RENDER_FAILED: { + pmEvent.bid = eventArgs.bid + pmEvent.message = eventArgs.message + pmEvent.reason = eventArgs.reason + break + } + default: + return + } + + sendEvent(pmEvent) +} + +function sendEvent(event) { + eventQueue.push(event) + logInfo(`${analyticsName} Event ${event.eventType}:`, event) + + if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + flush() + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: asteriobidAnalytics, + code: 'asteriobid' +}) + +asteriobidAnalytics.getOptions = function () { + return initOptions +} + +asteriobidAnalytics.flush = flush + +export default asteriobidAnalytics diff --git a/modules/asteriobidAnalyticsAdapter.md b/modules/asteriobidAnalyticsAdapter.md new file mode 100644 index 00000000000..524cf6e2721 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.md @@ -0,0 +1,41 @@ +# Overview + +Module Name: AsterioBid Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@asteriobid.com + +# Description +Analytics adapter for
AsterioBid. Contact admin@asteriobid.com for information. + +# Test Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78' + } +}); + +``` + +# Advanced Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78', + version: 'v1', // configuration version for the comparison + adUnitDict: { // provide names of the ad units for better reporting + adunitid1: 'Top Banner', + adunitid2: 'Bottom Banner' + }, + customParam: { // provide custom parameters values that you want to collect and report + param1: 'value1', + param2: 'value2' + } + } +}); + +``` diff --git a/modules/astraoneBidAdapter.js b/modules/astraoneBidAdapter.js index d7f92bb5fac..216257fb7bc 100644 --- a/modules/astraoneBidAdapter.js +++ b/modules/astraoneBidAdapter.js @@ -2,6 +2,13 @@ import { _map } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'astraone'; const SSP_ENDPOINT = 'https://ssp.astraone.io/auction/prebid'; const TTL = 60; @@ -94,7 +101,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} validBidRequests an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests(validBidRequests, bidderRequest) { diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 9beb20d4f77..92a4343b3ed 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,8 +1,7 @@ import { _each, deepAccess, - formatQS, - getBidIdParameter, + formatQS, getBidIdParameter, getValue, isArray, isFn, @@ -13,6 +12,15 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'audiencerun'; const BASE_URL = 'https://d.audiencerun.com'; const AUCTION_URL = `${BASE_URL}/prebid`; diff --git a/modules/automatadAnalyticsAdapter.js b/modules/automatadAnalyticsAdapter.js new file mode 100644 index 00000000000..436418e7597 --- /dev/null +++ b/modules/automatadAnalyticsAdapter.js @@ -0,0 +1,334 @@ +import { + logError, + logInfo, + logMessage +} from '../src/utils.js'; + +import CONSTANTS from '../src/constants.json'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { config } from '../src/config.js' + +/** Prebid Event Handlers */ + +const ADAPTER_CODE = 'automatadAnalytics' +const trialCountMilsMapping = [1500, 3000, 5000, 10000]; + +var isLoggingEnabled; var queuePointer = 0; var retryCount = 0; var timer = null; var __atmtdAnalyticsQueue = []; var qBeingUsed; var qTraversalComplete; + +const prettyLog = (level, text, isGroup = false, cb = () => {}) => { + if (self.isLoggingEnabled === undefined) { + if (window.localStorage.getItem('__aggLoggingEnabled')) { + self.isLoggingEnabled = true + } else { + const queryParams = new URLSearchParams(new URL(window.location.href).search) + self.isLoggingEnabled = queryParams.has('aggLoggingEnabled') + } + } + + if (self.isLoggingEnabled) { + if (isGroup) { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group Start ---`) + try { + cb(); + } catch (error) { + logError(`ATD Analytics Adapter: ERROR: ${'Error during cb function in prettyLog'}`) + } + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group End ---`) + } else { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text}`) + } + } +} + +const processEvents = () => { + if (self.retryCount === trialCountMilsMapping.length) { + self.prettyLog('error', `Aggregator still hasn't loaded. Processing que stopped`, trialCountMilsMapping, self.retryCount) + return; + } + + self.prettyLog('status', `Que has been inactive for a while. Adapter starting to process que now... Trial Count = ${self.retryCount + 1}`) + + let shouldTryAgain = false + + while (self.queuePointer < self.__atmtdAnalyticsQueue.length) { + const eventType = self.__atmtdAnalyticsQueue[self.queuePointer][0] + const args = self.__atmtdAnalyticsQueue[self.queuePointer][1] + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + window.atmtdAnalytics.auctionInitHandler(args); + } else { + shouldTryAgain = true + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } + break; + case 'slotRenderEnded': + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(args); + } else { + shouldTryAgain = true + } + break; + case 'impressionViewable': + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + window.atmtdAnalytics.impressionViewableHandler(args); + } else { + shouldTryAgain = true + } + break; + } + + if (shouldTryAgain) break; + } catch (error) { + self.prettyLog('error', `Unhandled Error while processing ${eventType} of ${self.queuePointer}th index in the que. Will not be retrying this raw event ...`, true, () => { + logError(`The error is `, error) + }) + } + + self.queuePointer = self.queuePointer + 1 + } + + if (shouldTryAgain) { + if (trialCountMilsMapping[self.retryCount]) self.prettyLog('warn', `Adapter failed to process event as aggregator has not loaded. Retrying in ${trialCountMilsMapping[self.retryCount]}ms ...`); + setTimeout(self.processEvents, trialCountMilsMapping[self.retryCount]) + self.retryCount = self.retryCount + 1 + } else { + self.qBeingUsed = false + self.qTraversalComplete = true + } +} + +const addGPTHandlers = () => { + const googletag = window.googletag || {} + googletag.cmd = googletag.cmd || [] + googletag.cmd.push(() => { + googletag.pubads().addEventListener('slotRenderEnded', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler && !self.qBeingUsed) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(event) + return; + } + self.__atmtdAnalyticsQueue.push(['slotRenderEnded', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting slotRenderEnded handler and pushing to que instead`) + }) + + googletag.pubads().addEventListener('impressionViewable', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler && !self.qBeingUsed) { + window.atmtdAnalytics.impressionViewableHandler(event) + return; + } + self.__atmtdAnalyticsQueue.push(['impressionViewable', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting impressionViewable handler and pushing to que instead`) + }) + }) +} + +const initializeQueue = () => { + self.__atmtdAnalyticsQueue.push = (args) => { + self.qBeingUsed = true + Array.prototype.push.apply(self.__atmtdAnalyticsQueue, [args]); + if (timer) { + clearTimeout(timer); + timer = null; + } + + if (args[0] === CONSTANTS.EVENTS.AUCTION_INIT) { + const timeout = parseInt(config.getConfig('bidderTimeout')) + 1500 + timer = setTimeout(() => { + self.processEvents() + }, timeout); + } else { + timer = setTimeout(() => { + self.processEvents() + }, 1500); + } + }; +} + +// ANALYTICS ADAPTER + +let baseAdapter = adapter({analyticsType: 'bundle'}); +let atmtdAdapter = Object.assign({}, baseAdapter, { + + disableAnalytics() { + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + const shouldNotPushToQueue = !self.qBeingUsed + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler && shouldNotPushToQueue) { + self.prettyLog('status', 'Aggregator loaded, initialising auction through handlers'); + window.atmtdAnalytics.auctionInitHandler(args); + } else { + self.prettyLog('warn', 'Aggregator not loaded, initialising auction through que ...'); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.bidRequestedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.bidRejectedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.bidResponseHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.bidderDoneHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.bidWonHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.noBidHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.auctionDebugHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler && shouldNotPushToQueue) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + } + } +}); + +atmtdAdapter.originEnableAnalytics = atmtdAdapter.enableAnalytics + +atmtdAdapter.enableAnalytics = function (configuration) { + if ((configuration === undefined && typeof configuration !== 'object') || configuration.options === undefined) { + logError('A valid configuration must be passed to the Atmtd Analytics Adapter.'); + return; + } + + const conf = configuration.options + + if (conf === undefined || typeof conf !== 'object' || conf.siteID === undefined || conf.publisherID === undefined) { + logError('A valid publisher ID and siteID must be passed to the Atmtd Analytics Adapter.'); + return; + } + + self.initializeQueue() + self.addGPTHandlers() + + window.__atmtdSDKConfig = { + publisherID: conf.publisherID, + siteID: conf.siteID, + collectDebugMessages: conf.logDebug ? conf.logDebug : false + } + + logMessage(`Automatad Analytics Adapter enabled with sdk config`, window.__atmtdSDKConfig) + + // eslint-disable-next-line + atmtdAdapter.originEnableAnalytics(configuration) +}; + +/// /////////// ADAPTER REGISTRATION ///////////// + +adapterManager.registerAnalyticsAdapter({ + adapter: atmtdAdapter, + code: ADAPTER_CODE +}); + +export var self = { + __atmtdAnalyticsQueue, + processEvents, + initializeQueue, + addGPTHandlers, + prettyLog, + queuePointer, + retryCount, + isLoggingEnabled, + qBeingUsed, + qTraversalComplete +} + +window.__atmtdAnalyticsGlobalObject = { + q: self.__atmtdAnalyticsQueue, + qBeingUsed: self.qBeingUsed, + qTraversalComplete: self.qTraversalComplete +} + +export default atmtdAdapter; diff --git a/modules/automatadAnalyticsAdapter.md b/modules/automatadAnalyticsAdapter.md new file mode 100644 index 00000000000..2be1af87f20 --- /dev/null +++ b/modules/automatadAnalyticsAdapter.md @@ -0,0 +1,23 @@ + +# Overview + +Module Name: Automatad Analytics Adapter +Module Type: Analytics Adapter +Maintainer: tech@automatad.com + +# Description + +Analytics adapter for automatad.com. Contact tech@automatad.com for information. + +# Test Parameters + +``` +{ + provider: 'automatadAnalytics', + options: { + publisherID: 'N8vZLx', + siteID: 'PXfvBq' + } +} + +``` \ No newline at end of file diff --git a/modules/axisBidAdapter.js b/modules/axisBidAdapter.js new file mode 100644 index 00000000000..8d7f2dd04fd --- /dev/null +++ b/modules/axisBidAdapter.js @@ -0,0 +1,210 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'axis'; +const AD_URL = 'https://prebid.axis-marketplace.com/pbjs'; +const SYNC_URL = 'https://cs.axis-marketplace.com'; + +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 || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { integration, token } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + integration, + token, + bidId, + schain, + bidfloor + }; + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.pos = mediaTypes[BANNER].pos; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.pos = mediaTypes[VIDEO].pos; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + placement.context = mediaTypes[VIDEO].context; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (e) { + logError(e); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.integration && params.token); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + iabCat: deepAccess(bidderRequest, 'ortb2.site.cat'), + coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout || 3000, + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + 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)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/axisBidAdapter.md b/modules/axisBidAdapter.md new file mode 100644 index 00000000000..d1625a56176 --- /dev/null +++ b/modules/axisBidAdapter.md @@ -0,0 +1,83 @@ +# Overview + +``` +Module Name: Axis Bidder Adapter +Module Type: Axis Bidder Adapter +Maintainer: help@axis-marketplace.com +``` + +# Description + +Connects to Axis exchange for bids. +Axis bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + } + ]; +``` diff --git a/modules/azerionedgeRtdProvider.js b/modules/azerionedgeRtdProvider.js new file mode 100644 index 00000000000..a162ce074aa --- /dev/null +++ b/modules/azerionedgeRtdProvider.js @@ -0,0 +1,143 @@ +/** + * This module adds the Azerion provider to the real time data module of prebid. + * + * The {@link module:modules/realTimeData} module is required + * @module modules/azerionedgeRtdProvider + * @requires module:modules/realTimeData + */ +import { submodule } from '../src/hook.js'; +import { mergeDeep } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +/** + * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const REAL_TIME_MODULE = 'realTimeData'; +const SUBREAL_TIME_MODULE = 'azerionedge'; +export const STORAGE_KEY = 'ht-pa-v1-a'; + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: SUBREAL_TIME_MODULE, +}); + +/** + * Get script url to load + * + * @param {Object} config + * + * @return {String} + */ +function getScriptURL(config) { + const VERSION = 'v1'; + const key = config.params?.key; + const publisherPath = key ? `${key}/` : ''; + return `https://edge.hyth.io/js/${VERSION}/${publisherPath}azerion-edge.min.js`; +} + +/** + * Attach script tag to DOM + * + * @param {Object} config + * + * @return {void} + */ +export function attachScript(config) { + const script = getScriptURL(config); + loadExternalScript(script, SUBREAL_TIME_MODULE, () => { + if (typeof window.azerionPublisherAudiences === 'function') { + window.azerionPublisherAudiences(config.params?.process || {}); + } + }); +} + +/** + * Fetch audiences info from localStorage. + * + * @return {Array} Audience ids. + */ +export function getAudiences() { + try { + const data = storage.getDataFromLocalStorage(STORAGE_KEY); + return JSON.parse(data).map(({ id }) => id); + } catch (_) { + return []; + } +} + +/** + * Pass audience data to configured bidders, using ORTB2 + * + * @param {Object} reqBidsConfigObj + * @param {Object} config + * @param {Array} audiences + * + * @return {void} + */ +export function setAudiencesToBidders(reqBidsConfigObj, config, audiences) { + const defaultBidders = ['improvedigital']; + const bidders = config.params?.bidders || defaultBidders; + bidders.forEach((bidderCode) => + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { + [bidderCode]: { + user: { + data: [ + { + name: 'azerionedge', + ext: { segtax: 4 }, + segment: audiences.map((id) => ({ id })), + }, + ], + }, + }, + }) + ); +} + +/** + * Module initialisation. + * + * @param {Object} config + * @param {Object} userConsent + * + * @return {boolean} + */ +function init(config, userConsent) { + attachScript(config); + return true; +} + +/** + * Real-time user audiences retrieval + * + * @param {Object} reqBidsConfigObj + * @param {function} callback + * @param {Object} config + * @param {Object} userConsent + * + * @return {void} + */ +export function getBidRequestData( + reqBidsConfigObj, + callback, + config, + userConsent +) { + const audiences = getAudiences(); + if (audiences.length > 0) { + setAudiencesToBidders(reqBidsConfigObj, config, audiences); + } + callback(); +} + +/** @type {RtdSubmodule} */ +export const azerionedgeSubmodule = { + name: SUBREAL_TIME_MODULE, + init: init, + getBidRequestData: getBidRequestData, +}; + +submodule(REAL_TIME_MODULE, azerionedgeSubmodule); diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md new file mode 100644 index 00000000000..2849bef3f63 --- /dev/null +++ b/modules/azerionedgeRtdProvider.md @@ -0,0 +1,112 @@ +--- +layout: page_v2 +title: azerion edge RTD Provider +display_name: Azerion Edge RTD Provider +description: Client-side contextual cookieless audiences. +page_type: module +module_type: rtd +module_code: azerionedgeRtdProvider +enable_download: true +vendor_specific: true +sidebarType: 1 +--- + +# Azerion Edge RTD Provider + +Client-side contextual cookieless audiences. + +Azerion Edge RTD module helps publishers to capture users' interest +audiences on their site, and attach these into the bid request. + +Maintainer: [azerion.com](https://www.azerion.com/) + +{:.no_toc} + +- TOC + {:toc} + +## Integration + +Compile the Azerion Edge RTD module (`azerionedgeRtdProvider`) into your Prebid build, +along with the parent RTD Module (`rtdModule`): + +```bash +gulp build --modules=rtdModule,azerionedgeRtdProvider +``` + +Set configuration via `pbjs.setConfig`. + +```js +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: 'azerionedge', + waitForIt: true, + params: { + key: '', + bidders: ['improvedigital'], + process: {} + } + } + ] + } + ... +} +``` + +### Parameter Description + +{: .table .table-bordered .table-striped } +| Name | Type | Description | Notes | +| :--- | :------- | :------------------ | :--------------- | +| name | `String` | RTD sub module name | Always "azerionedge" | +| waitForIt | `Boolean` | Required to ensure that the auction is delayed for the module to respond. | Optional. Defaults to false but recommended to true. | +| params.key | `String` | Publisher partner specific key | Optional | +| params.bidders | `Array` | Bidders with which to share segment information | Optional. Defaults to "improvedigital". | +| params.process | `Object` | Configuration for the Azerion Edge script. | Optional. Defaults to `{}`. | + +## Context + +As all data collection is on behalf of the publisher and based on the consent the publisher has +received from the user, this module does not require a TCF vendor configuration. Consent is +provided to the module when the user gives the relevant permissions on the publisher website. + +As Prebid.js utilizes TCF vendor consent for the RTD module to load, the module needs to be labeled +within the Vendor Exceptions. + +### Instructions + +If the Prebid GDPR enforcement is enabled, the module should be labeled +as exception, as shown below: + +```js +[ + { + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: ["azerionedge"] + }, + ... +] +``` + +## Testing + +To view an example: + +```bash +gulp serve-fast --modules=rtdModule,azerionedgeRtdProvider +``` + +Access [http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html) +in your browser. + +Run the unit tests: + +```bash +npm test -- --file "test/spec/modules/azerionedgeRtdProvider_spec.js" +``` diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 37de8e637a9..658fc30b43b 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -7,14 +7,15 @@ import { isFn, logWarn, parseSizesInput, - parseUrl + parseUrl, + formatQS } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; -const ADAPTER_VERSION = '1.19'; +const ADAPTER_VERSION = '1.20'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -22,6 +23,8 @@ const CURRENCY = 'USD'; export const VIDEO_ENDPOINT = 'https://reachms.bfmio.com/bid.json?exchange_id='; export const BANNER_ENDPOINT = 'https://display.bfmio.com/prebid_display'; export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const SYNC_IFRAME_ENDPOINT = 'https://sync.bfmio.com/sync_iframe'; +export const SYNC_IMAGE_ENDPOINT = 'https://sync.bfmio.com/syncb'; export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; @@ -152,11 +155,22 @@ export const spec = { } }, - getUserSyncs(syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '') { - let syncs = []; + getUserSyncs(syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = {}) { let { gdprApplies, consentString = '' } = gdprConsent; + let { gppString = '', applicableSections = [] } = gppConsent; let bannerResponse = find(serverResponses, (res) => isArray(res.body)); + let syncs = []; + let params = { + id: appId, + gdpr: gdprApplies ? 1 : 0, + gc: consentString, + gce: 1, + us_privacy: uspConsent, + gpp: gppString, + gpp_sid: Array.isArray(applicableSections) ? applicableSections.join(',') : '' + }; + if (bannerResponse) { if (syncOptions.iframeEnabled) { bannerResponse.body @@ -171,12 +185,12 @@ export const spec = { } else if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `https://sync.bfmio.com/sync_iframe?ifg=1&id=${appId}&gdpr=${gdprApplies ? 1 : 0}&gc=${consentString}&gce=1&us_privacy=${uspConsent}` + url: `${SYNC_IFRAME_ENDPOINT}?ifg=1&${formatQS(params)}` }); } else if (syncOptions.pixelEnabled) { syncs.push({ type: 'image', - url: `https://sync.bfmio.com/syncb?pid=144&id=${appId}&gdpr=${gdprApplies ? 1 : 0}&gc=${consentString}&gce=1&us_privacy=${uspConsent}` + url: `${SYNC_IMAGE_ENDPOINT}?pid=144&${formatQS(params)}` }); } @@ -404,6 +418,12 @@ function createVideoRequestData(bid, bidderRequest) { deepSetValue(payload, 'user.ext.consent', consentString); } + if (bidderRequest && bidderRequest.gppConsent) { + let { gppString, applicableSections } = bidderRequest.gppConsent; + deepSetValue(payload, 'regs.gpp', gppString); + deepSetValue(payload, 'regs.gpp_sid', applicableSections); + } + if (bid.schain) { deepSetValue(payload, 'source.ext.schain', bid.schain); } @@ -459,6 +479,12 @@ function createBannerRequestData(bids, bidderRequest) { payload.gdprConsent = consentString; } + if (bidderRequest && bidderRequest.gppConsent) { + let { gppString, applicableSections } = bidderRequest.gppConsent; + payload.gpp = gppString; + payload.gppSid = applicableSections; + } + if (bids[0] && bids[0].schain) { payload.schain = bids[0].schain; } diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index c5282c28cfc..0b2a965448b 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, getValue, isArray, logInfo, @@ -13,6 +12,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ + const BIDDER_CODE = 'beop'; const ENDPOINT_URL = 'https://hb.beop.io/bid'; const TCF_VENDOR_ID = 666; @@ -24,11 +29,11 @@ export const spec = { gvlid: TCF_VENDOR_ID, aliases: ['bp'], /** - * Test if the bid request is valid. - * - * @param {bid} : The Bid params - * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. - */ + * Test if the bid request is valid. + * + * @param {Bid} bid The Bid params + * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. + */ isBidRequestValid: function(bid) { const id = bid.params.accountId || bid.params.networkId; if (id === null || typeof id === 'undefined') { @@ -40,12 +45,12 @@ export const spec = { return bid.mediaTypes.banner !== null && typeof bid.mediaTypes.banner !== 'undefined'; }, /** - * Create a BeOp server request from a list of BidRequest - * - * @param {validBidRequests[], ...} : The array of validated bidRequests - * @param {... , bidderRequest} : Common params for each bidRequests - * @return ServerRequest Info describing the request to the BeOp's server - */ + * Create a BeOp server request from a list of BidRequest + * + * @param {validBidRequests} validBidRequests The array of validated bidRequests + * @param {BidderRequest} bidderRequest Common params for each bidRequests + * @return ServerRequest Info describing the request to the BeOp's server + */ buildRequests: function(validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); const firstPartyData = bidderRequest.ortb2 || {}; @@ -73,6 +78,7 @@ export const spec = { is_amp: deepAccess(bidderRequest, 'referrerInfo.isAmp'), gdpr_applies: gdpr ? gdpr.gdprApplies : false, tc_string: (gdpr && gdpr.gdprApplies) ? gdpr.consentString : null, + eids: firstSlot.eids, }; const payloadString = JSON.stringify(payloadObject); @@ -160,6 +166,7 @@ function beOpRequestSlotsMaker(bid) { brc: getBidIdParameter('bidRequestsCount', bid), bdrc: getBidIdParameter('bidderRequestCount', bid), bwc: getBidIdParameter('bidderWinsCount', bid), + eids: bid.userIdAsEids, } } diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index d615e433cc0..d2010f22e1a 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,6 +1,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getAdUnitSizes, parseSizesInput} from '../src/utils.js'; +import {parseSizesInput} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; @@ -22,7 +32,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @param {validBidRequests} validBidRequests an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { diff --git a/modules/bidViewability.js b/modules/bidViewability.js index a5cab99b1a7..be18095e369 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -67,6 +67,8 @@ export let logWinningBidNotFound = (slot) => { export let impressionViewableHandler = (globalModuleConfig, slot, event) => { let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); + let respectiveDeferredAdUnit = getGlobal().adUnits.find(adUnit => adUnit.deferBilling && respectiveBid.adUnitCode === adUnit.code); + if (respectiveBid === null) { logWinningBidNotFound(slot); } else { @@ -74,6 +76,11 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { fireViewabilityPixels(globalModuleConfig, respectiveBid); // trigger respective bidder's onBidViewable handler adapterManager.callBidViewableBidder(respectiveBid.adapterCode || respectiveBid.bidder, respectiveBid); + + if (respectiveDeferredAdUnit) { + adapterManager.callBidBillableBidder(respectiveBid); + } + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, respectiveBid); } diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 78a1539fb1a..922a4a9def4 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -2,19 +2,20 @@ Module Name: bidViewability -Purpose: Track when a bid is viewable +Purpose: Track when a bid is viewable (and also ready for billing) Maintainer: harshad.mane@pubmatic.com # Description -- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event -- Bidderes can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement the `onBidViewable` method to capture this event +- Bidders can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` - GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent . This event is fired when an impression becomes viewable, according to the Active View criteria. Refer: https://support.google.com/admanager/answer/4524488 -- The module does not work with adserver other than GAM with GPT integration +- This module does not work with any adserver's other than GAM with GPT integration - Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using param ```customMatchFunction``` -- When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters -- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs +- When a rendered PBJS bid is viewable the module will trigger a BID_VIEWABLE event, which can be consumed by bidders and analytics adapters +- If the viewable bid contains a ```vurls``` param containing URL's and the Bid Viewability module is configured with ``` firePixels: true ``` then the URLs mentioned in bid.vurls will be called. Please note that GDPR and USP related parameters will be added to the given URLs +- This module is also compatible with Prebid core's billing deferral logic, this means that bids linked to an ad unit marked with `deferBilling: true` will trigger a bid adapter's `onBidBillable` function (if present) indicating an ad slot was viewed and also billing ready (if it were deferred). # Params - enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable @@ -44,6 +45,6 @@ Refer: https://support.google.com/admanager/answer/4524488 ``` # Please Note: -- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative -- Works with Banner, Outsteam, Native creatives +- This module doesn't seem to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative +- Works with Banner, Outsteam and Native creatives diff --git a/modules/biddoBidAdapter.js b/modules/biddoBidAdapter.js index 5512ca60f8e..cf39c572629 100644 --- a/modules/biddoBidAdapter.js +++ b/modules/biddoBidAdapter.js @@ -1,6 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'biddo'; const ENDPOINT_URL = 'https://ad.adopx.net/delivery/impress'; diff --git a/modules/bidglassBidAdapter.js b/modules/bidglassBidAdapter.js index 3184372881b..7ae1ccf9217 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -1,7 +1,14 @@ -import { _each, isArray, getBidIdParameter, deepClone, getUniqueIdentifierStr } from '../src/utils.js'; -// import {config} from 'src/config.js'; +import {_each, isArray, deepClone, getUniqueIdentifierStr, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'bidglass'; export const spec = { @@ -19,7 +26,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest request by bidder * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { diff --git a/modules/big-richmediaBidAdapter.js b/modules/big-richmediaBidAdapter.js index 8a03aac1ace..ecb1724c2a1 100644 --- a/modules/big-richmediaBidAdapter.js +++ b/modules/big-richmediaBidAdapter.js @@ -3,6 +3,11 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {spec as baseAdapter} from './appnexusBidAdapter.js'; // eslint-disable-line prebid/validate-imports +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'big-richmedia'; const metadataByRequestId = {}; diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js index dc7731231ab..d2eba3f0f81 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/bizzclickBidAdapter.js @@ -1,322 +1,77 @@ -import {_map, deepAccess, deepSetValue, getDNT, logMessage, logWarn} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.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' +const SOURCE_ID_MACRO = '[sourceid]'; +const ACCOUNT_ID_MACRO = '[accountid]'; +const HOST_MACRO = '[host]'; +const URL = `https://${HOST_MACRO}.bizzclick.com/bid?rtb_seat_id=${SOURCE_ID_MACRO}&secret_key=${ACCOUNT_ID_MACRO}&integration_type=prebidjs`; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_HOST = 'us-e-node1'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 20, }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.ext = { + [BIDDER_CODE]: { + accountId: bidRequest.params.accountId, + sourceId: bidRequest.params.sourceId, + host: bidRequest.params.host || DEFAULT_HOST, + } + } + return imp; }, - body: { - id: 4, - name: 'data', - type: 2 + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.test = config.getConfig('debug') ? 1 : 0; + if (!request.cur) request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + return request; }, - cta: { - id: 1, - type: 12, - name: 'data' + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; } -}; -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) + return Boolean(bid.params.sourceId) && Boolean(bid.params.accountId); }, - /** - * 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) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - 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.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let bids = []; - for (let bidRequest of validBidRequests) { - let impObject = prepareImpObject(bidRequest); - let data = { - id: bidRequest.bidId, - test: config.getConfig('debug') ? 1 : 0, - at: 1, - cur: ['USD'], - device: { - w: winTop.screen.width, - h: winTop.screen.height, - dnt: getDNT() ? 1 : 0, - language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', - }, - site: { - page: location.pathname, - host: location.host - }, - source: { - tid: bidRequest.ortb2Imp?.ext?.tid, - ext: { - schain: {} - } - }, - regs: { - coppa: config.getConfig('coppa') === true ? 1 : 0, - ext: {} - }, - user: { - ext: {} - }, - ext: { - ts: Date.now() - }, - tmax: bidRequest.timeout, - imp: [impObject], - }; - - let connection = navigator.connection || navigator.webkitConnection; - if (connection && connection.effectiveType) { - data.device.connectiontype = connection.effectiveType; - } - if (bidRequest) { - if (bidRequest.schain) { - deepSetValue(data, 'source.ext.schain', bidRequest.schain); - } - - if (bidRequest.gdprConsent && bidRequest.gdprConsent.gdprApplies) { - deepSetValue(data, 'regs.ext.gdpr', bidRequest.gdprConsent.gdprApplies ? 1 : 0); - deepSetValue(data, 'user.ext.consent', bidRequest.gdprConsent.consentString); - } - - if (bidRequest.uspConsent !== undefined) { - deepSetValue(data, 'regs.ext.us_privacy', bidRequest.uspConsent); - } - } - bids.push(data) - } + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return []; + const { sourceId, accountId } = validBidRequests[0].params; + const host = validBidRequests[0].params.host || 'USE'; + const endpointURL = URL.replace(HOST_MACRO, host || DEFAULT_HOST) + .replace(ACCOUNT_ID_MACRO, accountId) + .replace(SOURCE_ID_MACRO, sourceId); + const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); return { method: 'POST', url: endpointURL, - data: bids + data: 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: (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 - }; - - bid.meta = {}; - if (response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length > 0) { - bid.meta.advertiserDomains = response.seatbid[0].bid[0].adomain; - } - switch (mediaType) { - case VIDEO: - bid.vastXml = response.seatbid[0].bid[0].adm - bid.vastUrl = response.seatbid[0].bid[0].ext.vastUrl - break - case NATIVE: - bid.native = parseNative(response.seatbid[0].bid[0].adm) - break - default: - bid.ad = response.seatbid[0].bid[0].adm - } - bids.push(bid); + interpretResponse: (response, request) => { + if (response?.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; } - return bids; + return []; }, }; -/** - * Determine type of request - * - * @param bidRequest - * @param type - * @returns {boolean} - */ -const checkRequestType = (bidRequest, type) => { - return (typeof 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 = { - // TODO: top-level ID is not in ORTB native 1.2, is this intentional? - // (despite the name, this appears to be an ORTB native request - not an imp - object) - id: bidRequest.bidId, - ver: NATIVE_VERSION, - }; - const assets = _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(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 { - 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 6fc1bebf546..ad342f34e07 100644 --- a/modules/bizzclickBidAdapter.md +++ b/modules/bizzclickBidAdapter.md @@ -11,94 +11,99 @@ Maintainer: support@bizzclick.com Module that connects to BizzClick SSP demand sources # Test Parameters -``` - var adUnits = [{ - code: 'placementId', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]] - } - }, - bids: [{ - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - }] - }, - { - code: 'native_example', - // sizes: [[1, 1]], - mediaTypes: { - native: { - title: { - required: true, - len: 800 - }, - image: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - } - } - }, - bids: [ { - bidder: '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 - ] - } }, +```js +const adUnits = [ + { + code: "placementId", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, + { + 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' - } - } - ] - } - ]; -``` \ No newline at end of file + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, + { + 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", + host: "host", + }, + }, + ], + }, +]; +``` diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 9a9d74d14c1..37c99878d68 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -2,8 +2,9 @@ // eslint-disable-next-line prebid/validate-imports import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' -import {_each, deepAccess, deepSetValue} from '../src/utils.js' +import { _each, deepAccess, deepSetValue, getWindowSelf, getWindowTop } from '../src/utils.js' export const BIDDER_CODE = 'bliink' +export const GVL_ID = 658 export const BLIINK_ENDPOINT_ENGINE = 'https://engine.bliink.io/prebid' export const BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME = 'https://tag.bliink.io/usersync.html' @@ -12,9 +13,10 @@ export const META_DESCRIPTION = 'description' const VIDEO = 'video' const BANNER = 'banner' - +window.bliinkBid = window.bliinkBid || {}; const supportedMediaTypes = [BANNER, VIDEO] const aliasBidderCode = ['bk'] +const CURRENCY = 'EUR'; /** * @description get coppa value from config @@ -23,6 +25,36 @@ function getCoppa() { return config.getConfig('coppa') === true ? 1 : 0; } +/** + * Retrieves the effective connection type from the browser's Navigator API. + * @returns {string} The effective connection type or 'unsupported' if unavailable. + */ +export function getEffectiveConnectionType() { + /** + * The effective connection type obtained from the browser's Navigator API. + * @type {string|undefined} + */ + const navigatorEffectiveType = navigator?.connection?.effectiveType; + + if (navigatorEffectiveType) { + return navigatorEffectiveType; + } + + return 'unsupported'; +} + +/** + * Retrieves the user IDs as EIDs from the first valid bid request. + * + * @param {Array} validBidRequests - Array of valid bid requests + * @returns {Array|undefined} - Array of user IDs as EIDs, or undefined if not found + */ +export function getUserIds(validBidRequests) { + /** @type {Object} */ + if (validBidRequests?.[0]?.userIdAsEids) { + return validBidRequests[0].userIdAsEids; + } +} export function getMetaList(name) { if (!name || name.length === 0) return [] @@ -91,8 +123,37 @@ export function getKeywords() { return []; } +function canAccessTopWindow() { + try { + if (getWindowTop().location.href) { + return true; + } + } catch (error) { + return false; + } +} + /** - * @param bidRequest + * domLoading feature is computed on window.top if reachable. + */ +export function getDomLoadingDuration() { + let domLoadingDuration = -1; + let performance; + + performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance; + + if (performance && performance.timing && performance.timing.navigationStart > 0) { + const val = performance.timing.domLoading - performance.timing.navigationStart; + if (val > 0) { + domLoadingDuration = val; + } + } + + return domLoadingDuration; +} + +/** + * @param bidResponse * @return {({cpm, netRevenue: boolean, requestId, width: number, currency, ttl: number, creativeId, height: number}&{mediaType: string, vastXml})|null} */ export const buildBid = (bidResponse) => { @@ -120,7 +181,7 @@ export const buildBid = (bidResponse) => { } return Object.assign(bid, { cpm: bidResponse.price, - currency: bidResponse.currency || 'EUR', + currency: bidResponse.currency || CURRENCY, creativeId: deepAccess(bidResponse, 'extras.deal_id'), requestId: deepAccess(bidResponse, 'extras.transaction_id'), width: deepAccess(bidResponse, `creative.${bid.mediaType}.width`) || 1, @@ -149,29 +210,59 @@ export const isBidRequestValid = (bid) => { */ export const buildRequests = (validBidRequests, bidderRequest) => { if (!validBidRequests || !bidderRequest || !bidderRequest.bids) return null - + const domLoadingDuration = getDomLoadingDuration().toString(); const tags = bidderRequest.bids.map((bid) => { - return { + let bidFloor; + const sizes = bid.sizes.map((size) => ({ w: size[0], h: size[1] })); + const mediaTypes = Object.keys(bid.mediaTypes) + if (typeof bid.getFloor === 'function') { + bidFloor = bid.getFloor({ + currency: CURRENCY, + mediaType: mediaTypes[0], + size: sizes[0] + }); + } + const id = bid.params.tagId + const request = { sizes: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), - id: bid.params.tagId, + id, // TODO: bidId is globally unique, is it a good choice for transaction ID (vs ortb2Imp.ext.tid)? transactionId: bid.bidId, - mediaTypes: Object.keys(bid.mediaTypes), + mediaTypes: mediaTypes, imageUrl: deepAccess(bid, 'params.imageUrl', ''), - }; + videoUrl: deepAccess(bid, 'params.videoUrl', ''), + refresh: (window.bliinkBid[id] = (window.bliinkBid[id] ?? -1) + 1) || undefined, + } + if (bidFloor) { + request.bidFloor = bidFloor + } + return request; }); let request = { tags, pageTitle: document.title, - pageUrl: deepAccess(bidderRequest, 'refererInfo.page'), + pageUrl: deepAccess(bidderRequest, 'refererInfo.page').replace(/\?.*$/, ''), pageDescription: getMetaValue(META_DESCRIPTION), keywords: getKeywords().join(','), + ect: getEffectiveConnectionType(), }; + const schain = deepAccess(validBidRequests[0], 'schain') + const eids = getUserIds(validBidRequests) + const device = bidderRequest.ortb2?.device if (schain) { request.schain = schain } + if (domLoadingDuration > -1) { + request.domLoadingDuration = domLoadingDuration + } + if (device) { + request.device = device + } + if (eids) { + request.eids = eids + } const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { request.gdpr = true @@ -183,7 +274,6 @@ export const buildRequests = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'uspConsent', bidderRequest.uspConsent); } - return { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, @@ -195,7 +285,6 @@ export const buildRequests = (validBidRequests, bidderRequest) => { * @description Parse the response (from buildRequests) and generate one or more bid objects. * * @param serverResponse - * @param request * @return */ const interpretResponse = (serverResponse) => { @@ -254,6 +343,7 @@ const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent) => */ export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: aliasBidderCode, supportedMediaTypes: supportedMediaTypes, isBidRequestValid, diff --git a/modules/blueconicRtdProvider.js b/modules/blueconicRtdProvider.js index b6eb9374671..c09fc6ee34c 100644 --- a/modules/blueconicRtdProvider.js +++ b/modules/blueconicRtdProvider.js @@ -11,6 +11,10 @@ import {submodule} from '../src/hook.js'; import {mergeDeep, isPlainObject, logMessage, logError} from '../src/utils.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'blueconic'; @@ -19,9 +23,9 @@ export const RTD_LOCAL_NAME = 'bcPrebidData'; export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** -* Try parsing stringified array of data. -* @param {String} data -*/ + * Try parsing stringified array of data. + * @param {String} data + */ function parseJson(data) { try { return JSON.parse(data); @@ -33,9 +37,8 @@ function parseJson(data) { /** * Add real-time data & merge segments. - * @param {Object} bidConfig + * @param {Object} ortb2 * @param {Object} rtd - * @param {Object} rtdConfig */ export function addRealTimeData(ortb2, rtd) { if (isPlainObject(rtd.ortb2)) { @@ -78,7 +81,7 @@ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent /** * Module init * @param {Object} provider - * @param {Objkect} userConsent + * @param {Object} userConsent * @return {boolean} */ function init(provider, userConsent) { diff --git a/modules/boldwinBidAdapter.js b/modules/boldwinBidAdapter.js index 3915df8b976..c7def383b5e 100644 --- a/modules/boldwinBidAdapter.js +++ b/modules/boldwinBidAdapter.js @@ -5,7 +5,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'boldwin'; const AD_URL = 'https://ssp.videowalldirect.com/pbjs'; -const SYNC_URL = 'https://cs.videowalldirect.com' +const SYNC_URL = 'https://sync.videowalldirect.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -80,6 +80,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 30844c9c483..2d9dcdfdf48 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -11,6 +11,10 @@ import {loadExternalScript} from '../src/adloader.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'brandmetrics' const MODULE_CODE = MODULE_NAME const RECEIVED_EVENTS = [] @@ -21,13 +25,14 @@ let billableEventsInitialized = false function init (config, userConsent) { const hasConsent = checkConsent(userConsent) + const initialize = hasConsent !== false - if (hasConsent) { + if (initialize) { const moduleConfig = getMergedConfig(config) initializeBrandmetrics(moduleConfig.params.scriptId) initializeBillableEvents() } - return hasConsent + return initialize } /** @@ -36,43 +41,46 @@ function init (config, userConsent) { * @returns {boolean} */ function checkConsent (userConsent) { - let consent = false - - if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { - const gdpr = userConsent.gdpr - - if (gdpr.vendorData) { - const vendor = gdpr.vendorData.vendor - const purpose = gdpr.vendorData.purpose - - let vendorConsent = false - if (vendor.consents) { - vendorConsent = vendor.consents[GVL_ID] + let consent + + if (userConsent) { + if (userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr + + if (gdpr.vendorData) { + const vendor = gdpr.vendorData.vendor + const purpose = gdpr.vendorData.purpose + + let vendorConsent = false + if (vendor.consents) { + vendorConsent = vendor.consents[GVL_ID] + } + + if (vendor.legitimateInterests) { + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] + } + + const purposes = TCF_PURPOSES.map(id => { + return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) + }) + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length + consent = vendorConsent && purposesValid } - - if (vendor.legitimateInterests) { - vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] - } - - const purposes = TCF_PURPOSES.map(id => { - return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) - }) - const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length - consent = vendorConsent && purposesValid + } else if (userConsent.usp) { + const usp = userConsent.usp + consent = usp[1] !== 'N' && usp[2] !== 'Y' } - } else if (userConsent.usp) { - const usp = userConsent.usp - consent = usp[1] !== 'N' && usp[2] !== 'Y' } return consent } /** -* Add event- listeners to hook in to brandmetrics events -* @param {Object} reqBidsConfigObj -* @param {function} callback -*/ + * Add event- listeners to hook in to brandmetrics events + * @param {Object} reqBidsConfigObj + * @param {Object} moduleConfig + * @param {function} callback + */ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { const callBidTargeting = (event) => { if (event.available && event.conf) { @@ -107,6 +115,7 @@ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { /** * Sets bid targeting of specific bidders * @param {Object} reqBidsConfigObj + * @param {Object} moduleConfig * @param {string} key Targeting key * @param {string} val Targeting value */ @@ -136,8 +145,8 @@ function initializeBrandmetrics(scriptId) { } /** -* Hook in to brandmetrics creative_in_view- event and emit billable- event for creatives measured by brandmetrics. -*/ + * Hook in to brandmetrics creative_in_view- event and emit billable- event for creatives measured by brandmetrics. + */ function initializeBillableEvents() { if (!billableEventsInitialized) { window._brandmetrics.push({ diff --git a/modules/braveBidAdapter.js b/modules/braveBidAdapter.js index d954522ae24..4c5448482db 100644 --- a/modules/braveBidAdapter.js +++ b/modules/braveBidAdapter.js @@ -4,6 +4,11 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'brave'; const DEFAULT_CUR = 'USD'; const ENDPOINT_URL = `https://point.bravegroup.tv/?t=2&partner=hash`; diff --git a/modules/bridBidAdapter.js b/modules/bridBidAdapter.js index 8e7c2f166ef..e784ea517ac 100644 --- a/modules/bridBidAdapter.js +++ b/modules/bridBidAdapter.js @@ -3,6 +3,12 @@ import {VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ + const SOURCE = 'pbjs'; const BIDDER_CODE = 'brid'; const ENDPOINT_URL = 'https://pbs.prebrid.tv/openrtb2/auction'; diff --git a/modules/bridgewellBidAdapter.js b/modules/bridgewellBidAdapter.js index 6088cefaa55..578acf8a358 100644 --- a/modules/bridgewellBidAdapter.js +++ b/modules/bridgewellBidAdapter.js @@ -4,6 +4,11 @@ import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'bridgewell'; const REQUEST_ENDPOINT = 'https://prebid.scupio.com/recweb/prebid.aspx?cb='; const BIDDER_VERSION = '1.1.0'; diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index c4cc5394a03..1fa1dac4e95 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -1,4 +1,17 @@ -import { getBidIdParameter, _each, isArray, getWindowTop, getUniqueIdentifierStr, deepSetValue, logError, logWarn, createTrackPixelHtml, getWindowSelf, isFn, isPlainObject } from '../src/utils.js'; +import { + _each, + isArray, + getWindowTop, + getUniqueIdentifierStr, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/brightcomSSPBidAdapter.js b/modules/brightcomSSPBidAdapter.js index b85a01c8fc7..4750881da40 100644 --- a/modules/brightcomSSPBidAdapter.js +++ b/modules/brightcomSSPBidAdapter.js @@ -1,5 +1,4 @@ import { - getBidIdParameter, isArray, getWindowTop, getUniqueIdentifierStr, @@ -9,7 +8,7 @@ import { createTrackPixelHtml, getWindowSelf, isFn, - isPlainObject, + isPlainObject, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js index b75fe9424b1..dcc365faaac 100644 --- a/modules/britepoolIdSystem.js +++ b/modules/britepoolIdSystem.js @@ -10,6 +10,13 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; const PIXEL = 'https://px.britepool.com/new?partner_id=t'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams + */ + /** @type {Submodule} */ export const britepoolIdSubmodule = { /** @@ -31,7 +38,7 @@ export const britepoolIdSubmodule = { * @function * @param {SubmoduleConfig} [submoduleConfig] * @param {ConsentData|undefined} consentData - * @returns {function(callback:function)} + * @returns {function} */ getId(submoduleConfig, consentData) { const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; diff --git a/modules/browsiBidAdapter.js b/modules/browsiBidAdapter.js index 03b6b2a8f3d..fa1cacaa568 100644 --- a/modules/browsiBidAdapter.js +++ b/modules/browsiBidAdapter.js @@ -3,6 +3,11 @@ import {config} from '../src/config.js'; import {VIDEO} from '../src/mediaTypes.js'; import {logError, logInfo, isArray, isStr} from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'browsi'; const DATA = 'brwvidtag'; const ADAPTER = '__bad'; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 4a61f40600d..ab3db2a5d20 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -25,6 +25,11 @@ import {getGlobal} from '../src/prebidGlobal.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'browsi'; const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); @@ -88,6 +93,7 @@ export function collectData() { let predictorData = { ...{ sk: _moduleParams.siteKey, + pk: _moduleParams.pubKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, url: `${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`, @@ -134,7 +140,6 @@ function getRTD(auc) { const adSlot = getSlotByCode(uc); const identifier = adSlot ? getMacroId(_browsiData['pmd'], adSlot) : uc; const _pd = _bp[identifier]; - rp[uc] = getKVObject(-1); if (!_pd) { return rp } @@ -186,7 +191,6 @@ function getAllSlots() { /** * get prediction and return valid object for key value set * @param {number} p - * @param {string?} keyName * @return {Object} key:value */ function getKVObject(p) { @@ -275,7 +279,7 @@ function getPredictionsFromServer(url) { if (req.status === 200) { try { const data = JSON.parse(response); - if (data && data.p && data.kn) { + if (data) { setData({p: data.p, kn: data.kn, pmd: data.pmd, bet: data.bet}); } else { setData({}); diff --git a/modules/bucksenseBidAdapter.js b/modules/bucksenseBidAdapter.js index 7b6c3911ea1..5aa14f2a53b 100644 --- a/modules/bucksenseBidAdapter.js +++ b/modules/bucksenseBidAdapter.js @@ -2,12 +2,18 @@ import { logInfo } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const WHO = 'BKSHBID-005'; const BIDDER_CODE = 'bucksense'; const URL = 'https://directo.prebidserving.com/prebidjs/'; export const spec = { code: BIDDER_CODE, + gvlid: 235, supportedMediaTypes: [BANNER], /** @@ -15,7 +21,7 @@ export const spec = { * * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. - */ + */ isBidRequestValid: function (bid) { logInfo(WHO + ' isBidRequestValid() - INPUT bid:', bid); if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -28,10 +34,10 @@ export const spec = { }, /** - * 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. + * 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: function (validBidRequests, bidderRequest) { logInfo(WHO + ' buildRequests() - INPUT validBidRequests:', validBidRequests, 'INPUT bidderRequest:', bidderRequest); @@ -73,7 +79,7 @@ export const spec = { * * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. - */ + */ interpretResponse: function (serverResponse, request) { logInfo(WHO + ' interpretResponse() - INPUT serverResponse:', serverResponse, 'INPUT request:', request); diff --git a/modules/buzzoolaBidAdapter.js b/modules/buzzoolaBidAdapter.js index b5ea6227f58..ae77ee159bc 100644 --- a/modules/buzzoolaBidAdapter.js +++ b/modules/buzzoolaBidAdapter.js @@ -5,6 +5,12 @@ import {Renderer} from '../src/Renderer.js'; import {OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'buzzoola'; const ENDPOINT = 'https://exchange.buzzoola.com/ssp/prebidjs'; const RENDERER_SRC = 'https://tube.buzzoola.com/new/build/buzzlibrary.js'; @@ -47,7 +53,6 @@ export const spec = { * Unpack the response from the server into a list of bids. * * @param {ServerResponse} serverResponse A successful response from the server. - * @param bidderRequest * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function ({body}, {data}) { diff --git a/modules/c1xBidAdapter.js b/modules/c1xBidAdapter.js index 8c9407825ba..79ba8cf499d 100644 --- a/modules/c1xBidAdapter.js +++ b/modules/c1xBidAdapter.js @@ -2,6 +2,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { logInfo, logError } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'c1x'; const URL = 'https://hb-stg.c1exchange.com/ht'; // const PIXEL_ENDPOINT = '//px.c1exchange.com/pubpixel/'; diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index 26e8639154c..e73564dacdb 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isPlainObject, @@ -170,6 +169,22 @@ export const cadentAdapter = { return cadentData; }, + + getGpp: (bidRequest, cadentData) => { + if (bidRequest.gppConsent) { + const {gppString: gpp, applicableSections: gppSid} = bidRequest.gppConsent; + if (cadentData.regs) { + cadentData.regs.gpp = gpp; + cadentData.regs.gpp_sid = gppSid; + } else { + cadentData.regs = { + gpp: gpp, + gpp_sid: gppSid + } + } + } + return cadentData; + }, getSupplyChain: (bidderRequest, cadentData) => { if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) { cadentData.source = { @@ -261,7 +276,7 @@ export const spec = { let isVideo = !!bid.mediaTypes.video; let data = { id: bid.bidId, - tid: bid.transactionId, + tid: bid.ortb2Imp?.ext?.tid, tagid, secure }; @@ -281,7 +296,7 @@ export const spec = { }); let cadentData = { - id: bidderRequest.auctionId, + id: bidderRequest.auctionId ?? bidderRequest.bidderRequestId, imp: cadentImps, device, site, @@ -290,6 +305,7 @@ export const spec = { }; cadentData = cadentAdapter.getGdpr(bidderRequest, Object.assign({}, cadentData)); + cadentData = cadentAdapter.getGpp(bidderRequest, Object.assign({}, cadentData)); cadentData = cadentAdapter.getSupplyChain(bidderRequest, Object.assign({}, cadentData)); if (bidderRequest && bidderRequest.uspConsent) { cadentData.us_privacy = bidderRequest.uspConsent; @@ -357,7 +373,7 @@ export const spec = { } return cadentBidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncs = []; const consentParams = []; if (syncOptions.iframeEnabled) { @@ -373,6 +389,14 @@ export const spec = { if (uspConsent && typeof uspConsent.consentString === 'string') { consentParams.push(`usp=${uspConsent.consentString}`); } + if (gppConsent && typeof gppConsent === 'object') { + if (gppConsent.gppString && typeof gppConsent.gppString === 'string') { + consentParams.push(`gpp=${gppConsent.gppString}`); + } + if (gppConsent.applicableSections && typeof gppConsent.applicableSections === 'object') { + consentParams.push(`gpp_sid=${gppConsent.applicableSections}`); + } + } if (consentParams.length > 0) { url = url + '?' + consentParams.join('&'); } diff --git a/modules/cleanioRtdProvider.js b/modules/cleanioRtdProvider.js index 7d0f461108b..f9bed5357ee 100644 --- a/modules/cleanioRtdProvider.js +++ b/modules/cleanioRtdProvider.js @@ -12,6 +12,10 @@ import { logError, generateUUID, insertElement } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + // ============================ MODULE STATE =============================== /** diff --git a/modules/clickforceBidAdapter.js b/modules/clickforceBidAdapter.js index 92bc9b1bad2..be81ff1885c 100644 --- a/modules/clickforceBidAdapter.js +++ b/modules/clickforceBidAdapter.js @@ -2,6 +2,12 @@ import { _each } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'clickforce'; const ENDPOINT_URL = 'https://ad.holmesmind.com/adserver/prebid.json?cb=' + new Date().getTime() + '&hb=1&ver=1.21'; diff --git a/modules/codefuelBidAdapter.js b/modules/codefuelBidAdapter.js index 2548b20189b..a4accee3ce0 100644 --- a/modules/codefuelBidAdapter.js +++ b/modules/codefuelBidAdapter.js @@ -2,6 +2,15 @@ import {deepAccess, isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'codefuel'; const CURRENCY = 'USD'; @@ -10,11 +19,11 @@ export const spec = { supportedMediaTypes: [ BANNER ], aliases: ['ex'], // short code /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { if (bid.nativeParams) { return false; @@ -22,11 +31,11 @@ export const spec = { return !!(bid.params.placementId || (bid.params.member && bid.params.invCode)); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} validBidRequests - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const page = bidderRequest.refererInfo.page; const domain = bidderRequest.refererInfo.domain; @@ -78,11 +87,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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: (serverResponse, { bids }) => { if (!serverResponse.body) { return []; @@ -116,12 +125,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { return []; } diff --git a/modules/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index 380e1f5fc77..3b90529b6cc 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -3,6 +3,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import { config } from '../src/config.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + */ + const BIDDER_CODE = 'cointraffic'; const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; const DEFAULT_CURRENCY = 'EUR'; diff --git a/modules/coinzillaBidAdapter.js b/modules/coinzillaBidAdapter.js index 15731423c49..9ae2c74547d 100644 --- a/modules/coinzillaBidAdapter.js +++ b/modules/coinzillaBidAdapter.js @@ -1,6 +1,12 @@ import { parseSizesInput } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'coinzilla'; const ENDPOINT_URL = 'https://request.czilladx.com/serve/request.php'; @@ -77,7 +83,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, - ttl: bidRequest.timeout, + ttl: response.timeout, referrer: referrer, ad: response.ad, mediaType: response.mediaType, diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index b1ee8875422..5fe78ff932d 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -5,6 +5,11 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'colossusssp'; const G_URL = 'https://colossusssp.com/?c=o&m=multi'; const G_URL_SYNC = 'https://sync.colossusssp.com'; @@ -135,7 +140,7 @@ export const spec = { groupId: bid.params.group_id, bidId: bid.bidId, tid: bid.ortb2Imp?.ext?.tid, - eids: [], + eids: bid.userIdAsEids || [], floor: {} }; diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 127b049bc99..87ac96f2131 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -3,23 +3,23 @@ import { BANNER } from '../src/mediaTypes.js'; // import { logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; const BIDDER_CODE = 'conceptx'; -let ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; +const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; // const LOG_PREFIX = 'ConceptX: '; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { - return !!(bid.bidId); + return !!(bid.bidId && bid.params.site && bid.params.adunit); }, buildRequests: function (validBidRequests, bidderRequest) { // logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); const requests = []; - + let requestUrl = `${ENDPOINT_URL}` if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - ENDPOINT_URL += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; - ENDPOINT_URL += '&consentString=' + bidderRequest.gdprConsent.consentString; + requestUrl += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; + requestUrl += '&consentString=' + bidderRequest.gdprConsent.consentString; } for (var i = 0; i < validBidRequests.length; i++) { const requestParent = { adUnits: [], meta: {} }; @@ -33,7 +33,7 @@ export const spec = { requestParent.adUnits.push(adUnit); requests.push({ method: 'POST', - url: ENDPOINT_URL, + url: requestUrl, options: { withCredentials: false, }, @@ -51,6 +51,9 @@ export const spec = { return bidResponses } const firstBid = bidResponsesFromServer[0] + if (!firstBid) { + return bidResponses + } const firstSeat = firstBid.ads[0] const bidResponse = { requestId: firstSeat.requestId, diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index bf4079322ff..bd738a39bba 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -3,6 +3,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'concert'; const CONCERT_ENDPOINT = 'https://bids.concert.io'; @@ -41,6 +48,7 @@ export const spec = { prebidVersion: '$prebid.version$', pageUrl: bidderRequest.refererInfo.page, screen: [window.screen.width, window.screen.height].join('x'), + browserLanguage: window.navigator.language, debug: debugTurnedOn(), uid: getUid(bidderRequest, validBidRequests), optedOut: hasOptedOutOfPersonalization(), @@ -48,6 +56,7 @@ export const spec = { uspConsent: bidderRequest.uspConsent, gdprConsent: bidderRequest.gdprConsent, gppConsent: bidderRequest.gppConsent, + tdid: getTdid(bidderRequest, validBidRequests), } }; @@ -262,3 +271,11 @@ function getOffset(el) { }; } } + +function getTdid(bidderRequest, validBidRequests) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return null; + } + + return deepAccess(validBidRequests[0], 'userId.tdid') || null; +} diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js new file mode 100644 index 00000000000..0b840db6c26 --- /dev/null +++ b/modules/connatixBidAdapter.js @@ -0,0 +1,185 @@ +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; + +import { + deepAccess, + isFn, + logError, + isArray, + formatQS +} from '../src/utils.js'; + +import { + BANNER, +} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'connatix'; +const AD_URL = 'https://capi.connatix.com/rtb/hba'; +const DEFAULT_MAX_TTL = '3600'; +const DEFAULT_CURRENCY = 'USD'; + +/* + * Get the bid floor value from the bid object, either using the getFloor function or by accessing the 'params.bidfloor' property. + * If the bid floor cannot be determined, return 0 as a fallback value. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 143, + supportedMediaTypes: [BANNER], + + /* + * Validate the bid request. + * If the request is valid, Connatix is trying to obtain at least one bid. + * Otherwise, the request to the Connatix server is not made + */ + isBidRequestValid: (bid = {}) => { + const bidId = deepAccess(bid, 'bidId'); + const mediaTypes = deepAccess(bid, 'mediaTypes', {}); + const params = deepAccess(bid, 'params', {}); + const bidder = deepAccess(bid, 'bidder'); + + const banner = deepAccess(mediaTypes, BANNER, {}); + + const hasBidId = Boolean(bidId); + const isValidBidder = (bidder === BIDDER_CODE); + const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); + const hasSizes = mediaTypes[BANNER] ? isValidSize : false; + const hasRequiredBidParams = Boolean(params.placementId); + + const isValid = isValidBidder && hasBidId && hasSizes && hasRequiredBidParams; + if (!isValid) { + logError(`Invalid bid request: isValidBidder: ${isValidBidder} hasBidId: ${hasBidId}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); + } + return isValid; + }, + + /* + * Build the request payload by processing valid bid requests and extracting the necessary information. + * Determine the host and page from the bidderRequest's refferUrl, and include ccpa and gdpr consents. + * Return an object containing the request method, url, and the constructed payload. + */ + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + const bidRequests = validBidRequests.map(bid => { + const { + bidId, + mediaTypes, + params, + sizes, + } = bid; + return { + bidId, + mediaTypes, + sizes, + placementId: params.placementId, + floor: getBidFloor(bid), + }; + }); + + const requestPayload = { + ortb2: bidderRequest.ortb2, + gdprConsent: bidderRequest.gdprConsent, + uspConsent: bidderRequest.uspConsent, + gppConsent: bidderRequest.gppConsent, + refererInfo: bidderRequest.refererInfo, + bidRequests, + }; + + return { + method: 'POST', + url: AD_URL, + data: requestPayload + }; + }, + + /* + * Interpret the server response and create an array of bid responses by extracting and formatting + * relevant information such as requestId, cpm, ttl, width, height, creativeId, referrer and ad + * Returns an array of bid responses by extracting and formatting the server response + */ + interpretResponse: (serverResponse) => { + const responseBody = serverResponse.body; + const bids = responseBody.Bids; + + if (!isArray(bids)) { + return []; + } + + const referrer = responseBody.Referrer; + return bids.map(bidResponse => ({ + requestId: bidResponse.RequestId, + cpm: bidResponse.Cpm, + ttl: bidResponse.Ttl || DEFAULT_MAX_TTL, + currency: 'USD', + mediaType: BANNER, + netRevenue: true, + width: bidResponse.Width, + height: bidResponse.Height, + creativeId: bidResponse.CreativeId, + ad: bidResponse.Ad, + referrer: referrer, + })); + }, + + /* + * Determine the user sync type (either 'iframe' or 'image') based on syncOptions. + * Construct the sync URL by appending required query parameters such as gdpr, ccpa, and coppa consents. + * Return an array containing an object with the sync type and the constructed URL. + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + if (!syncOptions.iframeEnabled) { + return []; + } + + if (!serverResponses || !serverResponses.length) { + return []; + } + + const params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } else { + params['gdpr'] = 0; + } + + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = encodeURIComponent(gdprConsent.consentString); + } + } + + if (typeof uspConsent === 'string') { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + const syncUrl = serverResponses[0].body.UserSyncEndpoint; + const queryParams = Object.keys(params).length > 0 ? formatQS(params) : ''; + + const url = queryParams ? `${syncUrl}?${queryParams}` : syncUrl; + return [{ + type: 'iframe', + url + }]; + } +}; + +registerBidder(spec); diff --git a/modules/connatixBidAdapter.md b/modules/connatixBidAdapter.md new file mode 100644 index 00000000000..595c294e311 --- /dev/null +++ b/modules/connatixBidAdapter.md @@ -0,0 +1,54 @@ + +# Overview + +``` +Module Name: Connatix Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid_integration@connatix.com +``` + +# Description +Connects to Connatix demand source to fetch bids. +Please use ```connatix``` as the bidder code. + +# Configuration +Connatix requires that ```iframe``` is used for user syncing. + +Example configuration: +``` +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: '*', // represents all bidders + filter: 'include' + } + } + } +}); +``` + +# Test Parameters +``` +var adUnits = [ + { + code: '1', + mediaTypes: { + banner: { + sizes: [[640, 480], [320, 180]], + }, + }, + bids: [ + { + bidder: 'connatix', + params: { + placementId: 'e4984e88-9ff4-45a3-8b9d-33aabcad634e', // required + bidfloor: 2.5, // optional + }, + }, + // Add more bidders and their parameters as needed + ], + }, + // Define more ad units here if necessary +]; +``` \ No newline at end of file diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index e1c5b427264..2ebc68baa84 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -10,10 +10,17 @@ import {submodule} from '../src/hook.js'; import {includes} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {formatQS, isPlainObject, logError, parseUrl} from '../src/utils.js'; +import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'connectId'; const STORAGE_EXPIRY_DAYS = 365; const STORAGE_DURATION = 60 * 60 * 24 * 1000 * STORAGE_EXPIRY_DAYS; @@ -26,6 +33,16 @@ const PLACEHOLDER = '__PIXEL_ID__'; const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut'; const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid']; +const O_AND_O_DOMAINS = [ + 'yahoo.com', + 'aol.com', + 'aol.ca', + 'aol.de', + 'aol.co.uk', + 'engadget.com', + 'techcrunch.com', + 'autoblog.com', +]; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @@ -104,9 +121,11 @@ function syncLocalStorageToCookie() { } function isStale(storedIdData) { - if (isPlainObject(storedIdData) && storedIdData.lastSynced && - (storedIdData.lastSynced + VALID_ID_DURATION) <= Date.now()) { + if (isOAndOTraffic()) { return true; + } else if (isPlainObject(storedIdData) && storedIdData.lastSynced) { + const validTTL = storedIdData.ttl || VALID_ID_DURATION; + return storedIdData.lastSynced + validTTL <= Date.now(); } return false; } @@ -127,6 +146,17 @@ function getSiteHostname() { return pageInfo.hostname; } +function isOAndOTraffic() { + let referer = getRefererInfo().ref; + + if (referer) { + referer = parseUrl(referer).hostname; + const subDomains = referer.split('.'); + referer = subDomains.slice(subDomains.length - 2, subDomains.length).join('.'); + } + return O_AND_O_DOMAINS.indexOf(referer) >= 0; +} + /** @type {Submodule} */ export const connectIdSubmodule = { /** @@ -238,6 +268,13 @@ export const connectIdSubmodule = { responseObj.puid = params.puid || responseObj.puid; responseObj.lastSynced = Date.now(); responseObj.lastUsed = Date.now(); + if (isNumber(responseObj.ttl)) { + let validTTLMiliseconds = responseObj.ttl * 60 * 60 * 1000; + if (validTTLMiliseconds > VALID_ID_DURATION) { + validTTLMiliseconds = VALID_ID_DURATION; + } + responseObj.ttl = validTTLMiliseconds; + } storeObject(responseObj); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index d5665b318be..b40ef30f6bc 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -1,7 +1,9 @@ -import { deepSetValue, convertTypes, tryAppendQueryString, logWarn } from '../src/utils.js'; +import { deepSetValue, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 05447a890cb..346b241fc1f 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -250,7 +250,7 @@ export function resetConsentData() { /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + * @param {{cmp:string, timeout:number, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int)) */ export function setConsentConfig(config) { // if `config.gdpr`, `config.usp` or `config.gpp` exist, assume new config format. diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 88851adfda5..416430fb1c9 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -4,19 +4,18 @@ * and make it available for any GPP supported adapters to read/pass this information to * their system and for various other features/modules in Prebid.js. */ -import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isEmpty, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gppDataHandler} from '../src/adapterManager.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import { enrichFPD } from '../src/fpd/enrichment.js'; +import {enrichFPD} from '../src/fpd/enrichment.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {cmpClient} from '../libraries/cmp/cmpClient.js'; +import {cmpClient, MODE_CALLBACK, MODE_MIXED, MODE_RETURN} from '../libraries/cmp/cmpClient.js'; import {GreedyPromise} from '../src/utils/promise.js'; import {buildActivityParams} from '../src/activities/params.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; -const CMP_VERSION = 1; export let userCMP; export let consentTimeout; @@ -25,30 +24,280 @@ let staticConsentData; let consentData; let addedConsentHook = false; -// add new CMPs here, with their dedicated lookup function -const cmpCallMap = { - 'iab': lookupIabConsent, - 'static': lookupStaticConsentData -}; +function pipeCallbacks(fn, {onSuccess, onError}) { + new GreedyPromise((resolve) => resolve(fn())).then(onSuccess, (err) => { + if (err instanceof GPPError) { + onError(err.message, ...err.args); + } else { + onError(`GPP error:`, err); + } + }); +} -/** - * This function checks the state of the IAB gppData's applicableSections field (to ensure it's populated and has a valid value). - * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. - * @param gppData represents the IAB gppData object - * @returns {Array} - */ -function applicableSections(gppData) { - return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0 - ? gppData.applicableSections - : []; +function lookupStaticConsentData(callbacks) { + return pipeCallbacks(() => processCmpData(staticConsentData), callbacks); } -/** - * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP - */ -function lookupStaticConsentData({onSuccess, onError}) { - processCmpData(staticConsentData, {onSuccess, onError}); +const GPP_10 = '1.0'; +const GPP_11 = '1.1'; + +class GPPError { + constructor(message, arg) { + this.message = message; + this.args = arg == null ? [] : [arg]; + } +} + +export class GPPClient { + static CLIENTS = {}; + + static register(apiVersion, defaultVersion = false) { + this.apiVersion = apiVersion; + this.CLIENTS[apiVersion] = this; + if (defaultVersion) { + this.CLIENTS.default = this; + } + } + + static INST; + + /** + * Ping the CMP to set up an appropriate client for it, and initialize it. + * + * @param mkCmp + * @returns {Promise<[GPPClient,Promise<{}>]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - a promise to GPP data. + */ + static init(mkCmp = cmpClient) { + let inst = this.INST; + if (!inst) { + let err; + const reset = () => err && (this.INST = null); + inst = this.INST = this.ping(mkCmp).catch(e => { + err = true; + reset(); + throw e; + }); + reset(); + } + return inst.then(([client, pingData]) => [ + client, + client.initialized ? client.refresh() : client.init(pingData) + ]); + } + + /** + * Ping the CMP to determine its version and set up a client appropriate for it. + * + * @param mkCmp + * @returns {Promise<[GPPClient, {}]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - the result from pinging the CMP. + */ + static ping(mkCmp = cmpClient) { + const cmpOptions = { + apiName: '__gpp', + apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use) + }; + + // in 1.0, 'ping' should return pingData but ignore callback; + // in 1.1 it should not return anything but run the callback + // the following looks for either - but once the version is known, produce a client that knows whether the + // rest of the interactions should pick return values or pass callbacks + + const probe = mkCmp({...cmpOptions, mode: MODE_RETURN}); + return new GreedyPromise((resolve, reject) => { + if (probe == null) { + reject(new GPPError('GPP CMP not found')); + return; + } + let done = false; // some CMPs do both return value and callbacks - avoid repeating log messages + const pong = (result, success) => { + if (done) return; + if (success != null && !success) { + reject(result); + return; + } + if (result == null) return; + done = true; + const cmpVersion = result?.gppVersion; + const Client = this.getClient(cmpVersion); + if (cmpVersion !== Client.apiVersion) { + logWarn(`Unrecognized GPP CMP version: ${cmpVersion}. Continuing using GPP API version ${Client}...`); + } else { + logInfo(`Using GPP version ${cmpVersion}`); + } + const mode = Client.apiVersion === GPP_10 ? MODE_MIXED : MODE_CALLBACK; + const client = new Client( + cmpVersion, + mkCmp({...cmpOptions, mode}) + ); + resolve([client, result]); + }; + + probe({ + command: 'ping', + callback: pong + }).then((res) => pong(res, true), reject); + }).finally(() => { + probe && probe.close(); + }); + } + + static getClient(cmpVersion) { + return this.CLIENTS.hasOwnProperty(cmpVersion) ? this.CLIENTS[cmpVersion] : this.CLIENTS.default; + } + + #resolve; + #reject; + #pending = []; + + initialized = false; + + constructor(cmpVersion, cmp) { + this.apiVersion = this.constructor.apiVersion; + this.cmpVersion = cmp; + this.cmp = cmp; + [this.#resolve, this.#reject] = [0, 1].map(slot => (result) => { + while (this.#pending.length) { + this.#pending.pop()[slot](result); + } + }); + } + + /** + * initialize this client - update consent data if already available, + * and set up event listeners to also update on CMP changes + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + init(pingData) { + const ready = this.updateWhenReady(pingData); + if (!this.initialized) { + this.initialized = true; + this.cmp({ + command: 'addEventListener', + callback: (event, success) => { + if (success != null && !success) { + this.#reject(new GPPError('Received error response from CMP', event)); + } else if (event?.pingData?.cmpStatus === 'error') { + this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); + } else if (this.isCMPReady(event?.pingData || {}) && this.events.includes(event?.eventName)) { + this.#resolve(this.updateConsent(event.pingData)); + } + } + }); + } + return ready; + } + + refresh() { + return this.cmp({command: 'ping'}).then(this.updateWhenReady.bind(this)); + } + + /** + * Retrieve and store GPP consent data. + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + updateConsent(pingData) { + return this.getGPPData(pingData).then((data) => { + if (data == null || isEmpty(data)) { + throw new GPPError('Received empty response from CMP', data); + } + return processCmpData(data); + }).then((data) => { + logInfo('Retrieved GPP consent from CMP:', data); + return data; + }); + } + + /** + * Return a promise to GPP consent data, to be retrieved the next time the CMP signals it's ready. + * + * @returns {Promise<{}>} + */ + nextUpdate() { + return new GreedyPromise((resolve, reject) => { + this.#pending.push([resolve, reject]); + }); + } + + /** + * Return a promise to GPP consent data, to be retrieved immediately if the CMP is ready according to `pingData`, + * or as soon as it signals that it's ready otherwise. + * + * @param pingData + * @returns {Promise<{}>} + */ + updateWhenReady(pingData) { + return this.isCMPReady(pingData) ? this.updateConsent(pingData) : this.nextUpdate(); + } +} + +// eslint-disable-next-line no-unused-vars +class GPP10Client extends GPPClient { + static { + super.register(GPP_10); + } + + events = ['sectionChange', 'cmpStatus']; + + isCMPReady(pingData) { + return pingData.cmpStatus === 'loaded'; + } + + getGPPData(pingData) { + const parsedSections = GreedyPromise.all( + (pingData.supportedAPIs || pingData.apiSupport || []).map((api) => this.cmp({ + command: 'getSection', + parameter: api + }).catch(err => { + logWarn(`Could not retrieve GPP section '${api}'`, err); + }).then((section) => [api, section])) + ).then(sections => { + // parse single section object into [core, gpc] to uniformize with 1.1 parsedSections + return Object.fromEntries( + sections.filter(([_, val]) => val != null) + .map(([api, section]) => { + const subsections = [ + Object.fromEntries(Object.entries(section).filter(([k]) => k !== 'Gpc')) + ]; + if (section.Gpc != null) { + subsections.push({ + SubsectionType: 1, + Gpc: section.Gpc + }); + } + return [api, subsections]; + }) + ); + }); + return GreedyPromise.all([ + this.cmp({command: 'getGPPData'}), + parsedSections + ]).then(([gppData, parsedSections]) => Object.assign({}, gppData, {parsedSections})); + } +} + +// eslint-disable-next-line no-unused-vars +class GPP11Client extends GPPClient { + static { + super.register(GPP_11, true); + } + + events = ['sectionChange', 'signalStatus']; + + isCMPReady(pingData) { + return pingData.signalStatus === 'ready'; + } + + getGPPData(pingData) { + return GreedyPromise.resolve(pingData); + } } /** @@ -58,45 +307,16 @@ function lookupStaticConsentData({onSuccess, onError}) { * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) */ -export function lookupIabConsent({onSuccess, onError}, mkClient = cmpClient) { - const cmp = mkClient({ - apiName: '__gpp', - apiVersion: CMP_VERSION, - }); - if (!cmp) { - return onError('GPP CMP not found.'); - } - - const startupMsg = (cmp.isDirect) ? 'Detected GPP CMP API is directly accessible, calling it now...' - : 'Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'; - logInfo(startupMsg); - - cmp({ - command: 'addEventListener', - callback: function (evt) { - if (evt) { - logInfo(`Received a ${(cmp.isDirect ? 'direct' : 'postmsg')} response from GPP CMP for event`, evt); - if (evt.eventName === 'sectionChange' || evt.pingData.cmpStatus === 'loaded') { - cmp({command: 'getGPPData'}).then((gppData) => { - logInfo(`Received a ${cmp.isDirect ? 'direct' : 'postmsg'} response from GPP CMP for getGPPData`, gppData); - return GreedyPromise.all( - (gppData?.pingData?.supportedAPIs || []) - .map((name) => cmp({command: 'getSection', parameter: name}) - .catch(() => { logError(`Could not retrieve section data for GPP section '${name}'`) }) - .then((res) => [name, res])) - ).then((sections) => { - const sectionData = Object.fromEntries(sections.filter(([_, val]) => val != null)); - processCmpData({gppData, sectionData}, {onSuccess, onError}); - }) - }); - } else if (evt.pingData.cmpStatus === 'error') { - onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); - } - } - } - }); +export function lookupIabConsent({onSuccess, onError}, mkCmp = cmpClient) { + pipeCallbacks(() => GPPClient.init(mkCmp).then(([client, gppDataPm]) => gppDataPm), {onSuccess, onError}); } +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, + 'static': lookupStaticConsentData +}; + /** * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. * @@ -128,19 +348,19 @@ function loadConsentData(cb) { onError: function (msg, ...extraArgs) { done(null, true, msg, ...extraArgs); } - } + }; cmpCallMap[userCMP](callbacks); if (!isDone) { const onTimeout = () => { const continueToAuction = (data) => { done(data, false, 'GPP CMP did not load, continuing auction...'); - } - processCmpData(consentData, { + }; + pipeCallbacks(() => processCmpData(consentData), { onSuccess: continueToAuction, onError: () => continueToAuction(storeConsentData()) - }) - } + }); + }; if (consentTimeout === 0) { onTimeout(); } else { @@ -195,27 +415,15 @@ export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook( }); }); -/** - * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we call `onError` - * If it's good, then we store the value and call `onSuccess` - */ -function processCmpData(consentData, {onSuccess, onError}) { - function checkData() { - const gppString = consentData?.gppData?.gppString; - const gppSection = consentData?.gppData?.applicableSections; - - return !!( - (!Array.isArray(gppSection)) || - (Array.isArray(gppSection) && (!gppString || !isStr(gppString))) - ); - } - - if (checkData()) { - onError(`CMP returned unexpected value during lookup process.`, consentData); - } else { - onSuccess(storeConsentData(consentData)); +function processCmpData(consentData) { + if ( + (consentData?.applicableSections != null && !Array.isArray(consentData.applicableSections)) || + (consentData?.gppString != null && !isStr(consentData.gppString)) || + (consentData?.parsedSections != null && !isPlainObject(consentData.parsedSections)) + ) { + throw new GPPError('CMP returned unexpected value during lookup process.', consentData); } + return storeConsentData(consentData); } /** @@ -223,14 +431,14 @@ function processCmpData(consentData, {onSuccess, onError}) { * @param {{}} gppData the result of calling a CMP's `getGPPData` (or equivalent) * @param {{}} sectionData map from GPP section name to the result of calling a CMP's `getSection` (or equivalent) */ -export function storeConsentData({gppData, sectionData} = {}) { +export function storeConsentData(gppData = {}) { consentData = { - gppString: (gppData) ? gppData.gppString : undefined, - gppData: (gppData) || undefined, + gppString: gppData?.gppString, + applicableSections: gppData?.applicableSections || [], + parsedSections: gppData?.parsedSections || {}, + gppData: gppData }; - consentData.applicableSections = applicableSections(gppData); - consentData.apiVersion = CMP_VERSION; - consentData.sectionData = sectionData; + gppDataHandler.setConsentData(gppData); return consentData; } @@ -242,11 +450,12 @@ export function resetConsentData() { userCMP = undefined; consentTimeout = undefined; gppDataHandler.reset(); + GPPClient.INST = null; } /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + * @param {{cmp:string, timeout:number, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int)) */ export function setConsentConfig(config) { config = config && config.gpp; @@ -271,7 +480,7 @@ export function setConsentConfig(config) { if (userCMP === 'static') { if (isPlainObject(config.consentData)) { - staticConsentData = {gppData: config.consentData, sectionData: config.sectionData}; + staticConsentData = config.consentData; consentTimeout = 0; } else { logError(`consentManagement.gpp config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); @@ -290,6 +499,7 @@ export function setConsentConfig(config) { gppDataHandler.enable(); loadConsentData(); // immediately look up consent data to make it available without requiring an auction } + config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); export function enrichFPDHook(next, fpd) { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index fb65a76c87b..78ec13cb891 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -90,7 +90,7 @@ function lookupUspConsent({onSuccess, onError}) { cmp({ command: 'registerDeletion', - callback: adapterManager.callDataDeletionRequest + callback: (res, success) => (success == null || success) && adapterManager.callDataDeletionRequest(res) }).catch(e => { logError('Error invoking CMP `registerDeletion`:', e); }); @@ -202,7 +202,7 @@ export function resetConsentData() { /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function - * @param {object} config required; consentManagementUSP module config settings; usp (string), timeout (int), allowAuctionWithoutConsent (boolean) + * @param {object} config required; consentManagementUSP module config settings; usp (string), timeout (int) */ export function setConsentConfig(config) { config = config && config.usp; diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index 4e2a92fb594..30b081e53d3 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -3,6 +3,12 @@ import {config} from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'consumable'; const BASE_URI = 'https://e.serverbid.com/api/v2'; @@ -65,6 +71,11 @@ export const spec = { }; } + if (bidderRequest && bidderRequest.gppConsent && bidderRequest.gppConsent.gppString) { + data.gpp = bidderRequest.gppConsent.gppString; + data.gpp_sid = bidderRequest.gppConsent.applicableSections; + } + if (bidderRequest && bidderRequest.uspConsent) { data.ccpa = bidderRequest.uspConsent; } @@ -180,20 +191,26 @@ export const spec = { return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { let syncUrl = 'https://sync.serverbid.com/ss/' + siteId + '.html'; if (syncOptions.iframeEnabled) { if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); } else { - syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); + } + } + if (gppConsent && gppConsent.gppString) { + syncUrl = appendUrlParam(syncUrl, `gpp=${encodeURIComponent(gppConsent.gppString)}`); + if (gppConsent.applicableSections && gppConsent.applicableSections.length > 0) { + syncUrl = appendUrlParam(syncUrl, `gpp_sid=${encodeURIComponent(gppConsent.applicableSections.join(','))}`); } } - if (uspConsent && uspConsent.consentString) { - syncUrl = appendUrlParam(syncUrl, `us_privacy=${uspConsent.consentString}`); + if (uspConsent) { + syncUrl = appendUrlParam(syncUrl, `us_privacy=${encodeURIComponent(uspConsent)}`); } if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { diff --git a/modules/contentexchangeBidAdapter.js b/modules/contentexchangeBidAdapter.js index be5900407ea..a6aa9262061 100644 --- a/modules/contentexchangeBidAdapter.js +++ b/modules/contentexchangeBidAdapter.js @@ -7,6 +7,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'contentexchange'; const AD_URL = 'https://eu2.adnetwork.agency/pbjs'; const SYNC_URL = 'https://sync2.adnetwork.agency'; +const GVLID = 864; function isBidResponseValid (bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -88,6 +89,7 @@ function getBidFloor(bid) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid = {}) => { diff --git a/modules/contxtfulRtdProvider.js b/modules/contxtfulRtdProvider.js new file mode 100644 index 00000000000..6d4b2a2ce29 --- /dev/null +++ b/modules/contxtfulRtdProvider.js @@ -0,0 +1,150 @@ +/** + * Contxtful Technologies Inc + * This RTD module provides receptivity feature that can be accessed using the + * getReceptivity() function. The value returned by this function enriches the ad-units + * that are passed within the `getTargetingData` functions and GAM. + */ + +import { submodule } from '../src/hook.js'; +import { + logInfo, + logError, + isStr, + isEmptyStr, + buildUrl, +} from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; + +const MODULE_NAME = 'contxtful'; +const MODULE = `${MODULE_NAME}RtdProvider`; + +const CONTXTFUL_RECEPTIVITY_DOMAIN = 'api.receptivity.io'; + +let initialReceptivity = null; +let contxtfulModule = null; + +/** + * Init function used to start sub module + * @param { { params: { version: String, customer: String, hostname: String } } } config + * @return { Boolean } + */ +function init(config) { + logInfo(MODULE, 'init', config); + initialReceptivity = null; + contxtfulModule = null; + + try { + const {version, customer, hostname} = extractParameters(config); + initCustomer(version, customer, hostname); + return true; + } catch (error) { + logError(MODULE, error); + return false; + } +} + +/** + * Extract required configuration for the sub module. + * validate that all required configuration are present and are valid. + * Throws an error if any config is missing of invalid. + * @param { { params: { version: String, customer: String, hostname: String } } } config + * @return { { version: String, customer: String, hostname: String } } + * @throws params.{name} should be a non-empty string + */ +function extractParameters(config) { + const version = config?.params?.version; + if (!isStr(version) || isEmptyStr(version)) { + throw Error(`${MODULE}: params.version should be a non-empty string`); + } + + const customer = config?.params?.customer; + if (!isStr(customer) || isEmptyStr(customer)) { + throw Error(`${MODULE}: params.customer should be a non-empty string`); + } + + const hostname = config?.params?.hostname || CONTXTFUL_RECEPTIVITY_DOMAIN; + + return {version, customer, hostname}; +} + +/** + * Initialize sub module for a customer. + * This will load the external resources for the sub module. + * @param { String } version + * @param { String } customer + * @param { String } hostname + */ +function initCustomer(version, customer, hostname) { + const CONNECTOR_URL = buildUrl({ + protocol: 'https', + host: hostname, + pathname: `/${version}/prebid/${customer}/connector/p.js`, + }); + + const externalScript = loadExternalScript(CONNECTOR_URL, MODULE_NAME); + addExternalScriptEventListener(externalScript); +} + +/** + * Add event listener to the script tag for the expected events from the external script. + * @param { HTMLScriptElement } script + */ +function addExternalScriptEventListener(script) { + if (!script) { + return; + } + + script.addEventListener('initialReceptivity', ({ detail }) => { + let receptivityState = detail?.ReceptivityState; + if (isStr(receptivityState) && !isEmptyStr(receptivityState)) { + initialReceptivity = receptivityState; + } + }); + + script.addEventListener('rxEngineIsReady', ({ detail: api }) => { + contxtfulModule = api; + }); +} + +/** + * Return current receptivity. + * @return { { ReceptivityState: String } } + */ +function getReceptivity() { + return { + ReceptivityState: contxtfulModule?.GetReceptivity()?.ReceptivityState || initialReceptivity + }; +} + +/** + * Set targeting data for ad server + * @param { [String] } adUnits + * @param {*} _config + * @param {*} _userConsent + * @return {{ code: { ReceptivityState: String } }} + */ +function getTargetingData(adUnits, _config, _userConsent) { + logInfo(MODULE, 'getTargetingData'); + if (!adUnits) { + return {}; + } + + const receptivity = getReceptivity(); + if (!receptivity?.ReceptivityState) { + return {}; + } + + return adUnits.reduce((targets, code) => { + targets[code] = receptivity; + return targets; + }, {}); +} + +export const contxtfulSubmodule = { + name: MODULE_NAME, + init, + extractParameters, + getTargetingData, +}; + +submodule('realTimeData', contxtfulSubmodule); diff --git a/modules/contxtfulRtdProvider.md b/modules/contxtfulRtdProvider.md new file mode 100644 index 00000000000..dfefca2067a --- /dev/null +++ b/modules/contxtfulRtdProvider.md @@ -0,0 +1,65 @@ +# Overview + +**Module Name:** Contxtful RTD Provider +**Module Type:** RTD Provider +**Maintainer:** [prebid@contxtful.com](mailto:prebid@contxtful.com) + +# Description + +The Contxtful RTD module offers a unique feature—Receptivity. Receptivity is an efficiency metric, enabling the qualification of any instant in a session in real time based on attention. The core idea is straightforward: the likelihood of an ad’s success increases when it grabs attention and is presented in the right context at the right time. + +To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please contact [prebid@contxtful.com](mailto:prebid@contxtful.com). + +# Configuration + +## Build Instructions + +To incorporate this module into your `prebid.js`, compile the module using the following command: + +```sh +gulp build --modules=contxtfulRtdProvider, +``` + +## Module Configuration + +Configure the `contxtfulRtdProvider` by passing the required settings through the `setConfig` function in `prebid.js`. + +```js +import pbjs from 'prebid.js'; + +pbjs.setConfig({ + "realTimeData": { + "auctionDelay": 1000, + "dataProviders": [ + { + "name": "contxtful", + "waitForIt": true, + "params": { + "version": "", + "customer": "" + } + } + ] + } +}); +``` + +### Configuration Parameters + +| Name | Type | Scope | Description | +|------------|----------|----------|-------------------------------------------| +| `version` | `string` | Required | Specifies the API version of Contxtful. | +| `customer` | `string` | Required | Your unique customer identifier. | + +# Usage + +The `contxtfulRtdProvider` module loads an external JavaScript file and authenticates with Contxtful APIs. The `getTargetingData` function then adds a `ReceptivityState` to each ad slot, which can have one of two values: `Receptive` or `NonReceptive`. + +```json +{ + "adUnitCode1": { "ReceptivityState": "Receptive" }, + "adUnitCode2": { "ReceptivityState": "NonReceptive" } +} +``` + +This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. \ No newline at end of file diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index fd436e51461..ebcad38d866 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,33 +1,128 @@ import { - logWarn, - isStr, + buildUrl, deepAccess, - isArray, - getBidIdParameter, deepSetValue, - isEmpty, - _each, - convertTypes, - parseUrl, - mergeDeep, - buildUrl, - _map, - logError, + getBidIdParameter, + isArray, isFn, isPlainObject, + isStr, + logError, + logWarn, + mergeDeep, + parseUrl, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {ORTB_MTYPES} from '../libraries/ortbConverter/processors/mediaType.js'; // Maintainer: mediapsr@epsilon.com +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').Device} Device + */ + const GVLID = 24; const BIDDER_CODE = 'conversant'; export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const URL = 'https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'; +function setSiteId(bidRequest, request) { + if (bidRequest.params.site_id) { + if (request.site) { + request.site.id = bidRequest.params.site_id; + } + if (request.app) { + request.app.id = bidRequest.params.site_id; + } + } +} + +function setPubcid(bidRequest, request) { + // Add common id if available + const pubcid = getPubcid(bidRequest); + if (pubcid) { + deepSetValue(request, 'user.ext.fpc', pubcid); + } +} + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300 + }, + request: function (buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + request.at = 1; + if (context.bidRequests) { + const bidRequest = context.bidRequests[0]; + setSiteId(bidRequest, request); + setPubcid(bidRequest, request); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const data = { + secure: 1, + bidfloor: getBidFloor(bidRequest) || 0, + displaymanager: 'Prebid.js', + displaymanagerver: '$prebid.version$' + }; + copyOptProperty(bidRequest.params.tag_id, data, 'tagid'); + mergeDeep(imp, data, imp); + return imp; + }, + bidResponse: function (buildBidResponse, bid, context) { + if (!bid.price) return; + + // ensure that context.mediaType is set to banner or video otherwise + if (!context.mediaType && context.bidRequest.mediaTypes) { + const [type] = Object.keys(context.bidRequest.mediaTypes); + if (Object.values(ORTB_MTYPES).includes(type)) { + context.mediaType = type; + } + } + const bidResponse = buildBidResponse(bid, context); + return bidResponse; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids; + }, + overrides: { + imp: { + banner(fillBannerImp, imp, bidRequest, context) { + if (bidRequest.mediaTypes && !bidRequest.mediaTypes.banner) return; + if (bidRequest.params.position) { + // fillBannerImp looks for mediaTypes.banner.pos so put it under the right name here + mergeDeep(bidRequest, {mediaTypes: {banner: {pos: bidRequest.params.position}}}); + } + fillBannerImp(imp, bidRequest, context); + }, + video(fillVideoImp, imp, bidRequest, context) { + if (bidRequest.mediaTypes && !bidRequest.mediaTypes.video) return; + const videoData = {}; + copyOptProperty(bidRequest.params?.position, videoData, 'pos'); + copyOptProperty(bidRequest.params?.mimes, videoData, 'mimes'); + copyOptProperty(bidRequest.params?.maxduration, videoData, 'maxduration'); + copyOptProperty(bidRequest.params?.protocols, videoData, 'protocols'); + copyOptProperty(bidRequest.params?.api, videoData, 'api'); + imp.video = mergeDeep(videoData, imp.video); + fillVideoImp(imp, bidRequest, context); + } + }, + } +}); + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -65,148 +160,14 @@ export const spec = { return true; }, - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests - an array of bids - * @param bidderRequest - * @return {ServerRequest} Info describing the request to the server. - */ - buildRequests: function(validBidRequests, bidderRequest) { - const page = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.page : ''; - let siteId = ''; - let pubcid = null; - let pubcidName = '_pubcid'; - let bidurl = URL; - - const conversantImps = validBidRequests.map(function(bid) { - const bidfloor = getBidFloor(bid); - - siteId = getBidIdParameter('site_id', bid.params) || siteId; - pubcidName = getBidIdParameter('pubcid_name', bid.params) || pubcidName; - - const imp = { - id: bid.bidId, - secure: 1, - bidfloor: bidfloor || 0, - displaymanager: 'Prebid.js', - displaymanagerver: '$prebid.version$' - }; - if (bid.ortb2Imp) { - mergeDeep(imp, bid.ortb2Imp); - } - - copyOptProperty(bid.params.tag_id, imp, 'tagid'); - - if (isVideoRequest(bid)) { - const videoData = deepAccess(bid, 'mediaTypes.video') || {}; - const format = convertSizes(videoData.playerSize || bid.sizes); - const video = {}; - - if (format && format[0]) { - copyOptProperty(format[0].w, video, 'w'); - copyOptProperty(format[0].h, video, 'h'); - } - - copyOptProperty(bid.params.position || videoData.pos, video, 'pos'); - copyOptProperty(bid.params.mimes || videoData.mimes, video, 'mimes'); - copyOptProperty(bid.params.maxduration || videoData.maxduration, video, 'maxduration'); - copyOptProperty(bid.params.protocols || videoData.protocols, video, 'protocols'); - copyOptProperty(bid.params.api || videoData.api, video, 'api'); - - imp.video = video; - } else { - const bannerData = deepAccess(bid, 'mediaTypes.banner') || {}; - const format = convertSizes(bannerData.sizes || bid.sizes); - const banner = {format: format}; - - copyOptProperty(bid.params.position || bannerData.pos, banner, 'pos'); - - imp.banner = banner; - } - - if (bid.userId && bid.userId.pubcid) { - pubcid = bid.userId.pubcid; - } else if (bid.crumbs && bid.crumbs.pubcid) { - pubcid = bid.crumbs.pubcid; - } - if (bid.params.white_label_url) { - bidurl = bid.params.white_label_url; - } - - return imp; - }); - - const payload = { - id: bidderRequest.bidderRequestId, - imp: conversantImps, - source: { - tid: bidderRequest.ortb2?.source?.tid, - }, - site: { - id: siteId, - mobile: document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, - page: page - }, - device: getDevice(), - at: 1 - }; - - let userExt = {}; - - // pass schain object if it is present - const schain = deepAccess(validBidRequests, '0.schain'); - if (schain) { - deepSetValue(payload, 'source.ext.schain', schain); - } - - if (bidderRequest) { - if (bidderRequest.timeout) { - deepSetValue(payload, 'tmax', bidderRequest.timeout); - } - - // Add GDPR flag and consent string - if (bidderRequest.gdprConsent) { - userExt.consent = bidderRequest.gdprConsent.consentString; - - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { - deepSetValue(payload, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); - } - } - - if (bidderRequest.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - } - - if (!pubcid) { - pubcid = readStoredValue(pubcidName); - } - - // Add common id if available - if (pubcid) { - userExt.fpc = pubcid; - } - - // Add Eids if available - const eids = collectEids(validBidRequests); - if (eids.length > 0) { - userExt.eids = eids; - } - - // Only add the user object if it's not empty - if (!isEmpty(userExt)) { - payload.user = {ext: userExt}; - } - - const firstPartyData = bidderRequest.ortb2 || {}; - mergeDeep(payload, firstPartyData); - - return { + buildRequests: function(bidRequests, bidderRequest) { + const payload = converter.toORTB({bidderRequest, bidRequests}); + const result = { method: 'POST', - url: bidurl, + url: makeBidUrl(bidRequests[0]), data: payload, }; + return result; }, /** * Unpack the response from the server into a list of bids. @@ -216,59 +177,7 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidRequest) { - const bidResponses = []; - const requestMap = {}; - serverResponse = serverResponse.body; - - if (bidRequest && bidRequest.data && bidRequest.data.imp) { - _each(bidRequest.data.imp, imp => requestMap[imp.id] = imp); - } - - if (serverResponse && isArray(serverResponse.seatbid)) { - _each(serverResponse.seatbid, function(bidList) { - _each(bidList.bid, function(conversantBid) { - const responseCPM = parseFloat(conversantBid.price); - if (responseCPM > 0.0 && conversantBid.impid) { - const responseAd = conversantBid.adm || ''; - const responseNurl = conversantBid.nurl || ''; - const request = requestMap[conversantBid.impid]; - - const bid = { - requestId: conversantBid.impid, - currency: serverResponse.cur || 'USD', - cpm: responseCPM, - creativeId: conversantBid.crid || '', - ttl: 300, - netRevenue: true - }; - bid.meta = {}; - if (conversantBid.adomain && conversantBid.adomain.length > 0) { - bid.meta.advertiserDomains = conversantBid.adomain; - } - - if (request.video) { - if (responseAd.charAt(0) === '<') { - bid.vastXml = responseAd; - } else { - bid.vastUrl = responseAd; - } - - bid.mediaType = 'video'; - bid.width = request.video.w; - bid.height = request.video.h; - } else { - bid.ad = responseAd + ''; - bid.width = conversantBid.w; - bid.height = conversantBid.h; - } - - bidResponses.push(bid); - } - }) - }); - } - - return bidResponses; + return converter.fromORTB({request: bidRequest.data, response: serverResponse.body}); }, /** @@ -328,51 +237,18 @@ export const spec = { } }; -/** - * Determine do-not-track state - * - * @returns {boolean} - */ -function getDNT() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -/** - * Return openrtb device object that includes ua, width, and height. - * - * @returns {Device} Openrtb device object - */ -function getDevice() { - const language = navigator.language ? 'language' : 'userLanguage'; - return { - h: screen.height, - w: screen.width, - dnt: getDNT() ? 1 : 0, - language: navigator[language].split('-')[0], - make: navigator.vendor ? navigator.vendor : '', - ua: navigator.userAgent - }; -} - -/** - * Convert arrays of widths and heights to an array of objects with w and h properties. - * - * [[300, 250], [300, 600]] => [{w: 300, h: 250}, {w: 300, h: 600}] - * - * @param {Array.>} bidSizes - arrays of widths and heights - * @returns {object[]} Array of objects with w and h - */ -function convertSizes(bidSizes) { - let format; - if (Array.isArray(bidSizes)) { - if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { - format = [{w: bidSizes[0], h: bidSizes[1]}]; - } else { - format = _map(bidSizes, d => { return {w: d[0], h: d[1]}; }); - } +function getPubcid(bidRequest) { + let pubcid = null; + if (bidRequest.userId && bidRequest.userId.pubcid) { + pubcid = bidRequest.userId.pubcid; + } else if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { + pubcid = bidRequest.crumbs.pubcid; } - - return format; + if (!pubcid) { + const pubcidName = getBidIdParameter('pubcid_name', bidRequest.params) || '_pubcid'; + pubcid = readStoredValue(pubcidName); + } + return pubcid; } /** @@ -398,33 +274,6 @@ function copyOptProperty(src, dst, dstName) { } } -/** - * Collect IDs from validBidRequests and store them as an extended id array - * @param bidRequests valid bid requests - */ -function collectEids(bidRequests) { - const request = bidRequests[0]; // bidRequests have the same userId object - const eids = []; - if (isArray(request.userIdAsEids) && request.userIdAsEids.length > 0) { - // later following white-list can be converted to block-list if needed - const requiredSourceValues = { - 'epsilon.com': 1, - 'adserver.org': 1, - 'liveramp.com': 1, - 'criteo.com': 1, - 'id5-sync.com': 1, - 'parrable.com': 1, - 'liveintent.com': 1 - }; - request.userIdAsEids.forEach(function(eid) { - if (requiredSourceValues.hasOwnProperty(eid.source)) { - eids.push(eid); - } - }); - } - return eids; -} - /** * Look for a stored value from both cookie and local storage and return the first value found. * @param key Key for the search @@ -480,4 +329,12 @@ function getBidFloor(bid) { return floor } +function makeBidUrl(bid) { + let bidurl = URL; + if (bid.params.white_label_url) { + bidurl = bid.params.white_label_url; + } + return bidurl; +} + registerBidder(spec); diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js index 9e237ef2558..e076fb4b0bb 100755 --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -1,8 +1,8 @@ - import * as utils from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'cpmstar'; @@ -49,13 +49,13 @@ export const spec = { var bidRequest = validBidRequests[i]; var referer = bidderRequest.refererInfo.page ? bidderRequest.refererInfo.page : bidderRequest.refererInfo.domain; referer = encodeURIComponent(referer); - var e = utils.getBidIdParameter('endpoint', bidRequest.params); + var e = getBidIdParameter('endpoint', bidRequest.params); var ENDPOINT = e == 'dev' ? ENDPOINT_DEV : e == 'staging' ? ENDPOINT_STAGING : ENDPOINT_PRODUCTION; var mediaType = spec.getMediaType(bidRequest); var playerSize = spec.getPlayerSize(bidRequest); var videoArgs = '&fv=0' + (playerSize ? ('&w=' + playerSize[0] + '&h=' + playerSize[1]) : ''); var url = ENDPOINT + '?media=' + mediaType + (mediaType == VIDEO ? videoArgs : '') + - '&json=c_b&mv=1&poolid=' + utils.getBidIdParameter('placementId', bidRequest.params) + + '&json=c_b&mv=1&poolid=' + getBidIdParameter('placementId', bidRequest.params) + '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + '&requestid=' + bidRequest.bidId + '&referer=' + encodeURIComponent(referer); diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 8f7821173c1..74e732d313f 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,13 +1,14 @@ -import {convertCamelToUnderscore, convertTypes, getBidRequest, logError} from '../src/utils.js'; +import {getBidRequest, logError} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; @@ -184,12 +185,9 @@ function bidToTag(bid) { if (keywords.length) { tag.keywords = keywords; } - // TODO: why does this need to iterate through every ad unit? - let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); - if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { + if (bid.mediaTypes?.banner) { tag.ad_types.push(BANNER); } - if (tag.ad_types.length === 0) { delete tag.ad_types; } diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 993346df849..2c0cacb7909 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -3,13 +3,21 @@ import { loadExternalScript } from '../src/adloader.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { find } from '../src/polyfill.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { hasPurpose1Consent } from '../src/utils/gpdr.js'; import { Renderer } from '../src/Renderer.js'; import { OUTSTREAM } from '../src/video.js'; +import { ajax } from '../src/ajax.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ const GVLID = 91; export const ADAPTER_VERSION = 36; @@ -28,7 +36,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 136; +export const FAST_BID_VERSION_CURRENT = 144; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; @@ -122,7 +130,8 @@ export const spec = { return []; }, - /** f + /** + * f * @param {object} bid * @return {boolean} */ @@ -201,7 +210,7 @@ export const spec = { /** * @param {*} response * @param {ServerRequest} request - * @return {Bid[]} + * @return {Bid[] | {bids: Bid[], fledgeAuctionConfigs: object[]}} */ interpretResponse: (response, request) => { const body = response.body || response; @@ -215,57 +224,80 @@ export const spec = { } const bids = []; + const fledgeAuctionConfigs = []; if (body && body.slots && isArray(body.slots)) { body.slots.forEach(slot => { - const bidRequest = find(request.bidRequests, b => b.adUnitCode === slot.impid && (!b.params.zoneId || parseInt(b.params.zoneId) === slot.zoneid)); - const bidId = bidRequest.bidId; - const bid = { - requestId: bidId, - cpm: slot.cpm, - currency: slot.currency, - netRevenue: true, - ttl: slot.ttl || 60, - creativeId: slot.creativecode, - width: slot.width, - height: slot.height, - dealId: slot.deal, - }; - if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { - const pafResponseMeta = { - content_id: slot.ext.paf.content_id, - transmission: response.ext.paf.transmission + const bidRequest = getAssociatedBidRequest(request.bidRequests, slot); + if (bidRequest) { + const bidId = bidRequest.bidId; + const bid = { + requestId: bidId, + cpm: slot.cpm, + currency: slot.currency, + netRevenue: true, + ttl: slot.ttl || 60, + creativeId: slot.creativecode, + width: slot.width, + height: slot.height, + dealId: slot.deal, }; - bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); - } - if (slot.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); - } - if (slot.ext?.meta?.networkName) { - bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) - } - if (slot.native) { - if (bidRequest.params.nativeCallback) { - bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); - } else { - bid.native = createPrebidNativeAd(slot.native); - bid.mediaType = NATIVE; + if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { + const pafResponseMeta = { + content_id: slot.ext.paf.content_id, + transmission: response.ext.paf.transmission + }; + bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); } - } else if (slot.video) { - bid.vastUrl = slot.displayurl; - bid.mediaType = VIDEO; - const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - // if outstream video, add a default render for it. - if (context === OUTSTREAM) { - bid.renderer = createOutstreamVideoRenderer(slot); + if (slot.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); } - } else { - bid.ad = slot.creative; + if (slot.ext?.meta?.networkName) { + bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) + } + if (slot.ext?.dsa) { + bid.meta = Object.assign({}, bid.meta, { dsa: slot.ext.dsa }) + } + if (slot.native) { + if (bidRequest.params.nativeCallback) { + bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); + } else { + bid.native = createPrebidNativeAd(slot.native); + bid.mediaType = NATIVE; + } + } else if (slot.video) { + bid.vastUrl = slot.displayurl; + bid.mediaType = VIDEO; + const context = deepAccess(bidRequest, 'mediaTypes.video.context'); + // if outstream video, add a default render for it. + if (context === OUTSTREAM) { + bid.renderer = createOutstreamVideoRenderer(slot); + } + } else { + bid.ad = slot.creative; + } + bids.push(bid); + } + }); + } + + if (isArray(body.ext?.igi)) { + body.ext.igi.forEach((igi) => { + if (isArray(igi?.igs)) { + igi.igs.forEach((igs) => { + fledgeAuctionConfigs.push(igs); + }); } - bids.push(bid); }); } + if (fledgeAuctionConfigs.length) { + return { + bids, + fledgeAuctionConfigs, + }; + } + return bids; }, /** @@ -306,6 +338,23 @@ export const spec = { adapter.handleSetTargeting(bid); } }, + + /** + * @param {BidRequest[]} bidRequests + */ + onDataDeletionRequest: (bidRequests) => { + const id = readFromAllStorages(BUNDLE_COOKIE_NAME); + if (id) { + deleteFromAllStorages(BUNDLE_COOKIE_NAME); + ajax('https://privacy.criteo.com/api/privacy/datadeletionrequest', + null, + JSON.stringify({ publisherUserId: id }), + { + contentType: 'application/json', + method: 'POST' + }); + } + } }; function readFromAllStorages(name) { @@ -426,17 +475,16 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { let networkId; let schain; let userIdAsEids; + let regs = Object.assign({}, { + coppa: bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined) + }, bidderRequest.ortb2?.regs); const request = { id: generateUUID(), publisher: { url: context.url, ext: bidderRequest.publisherExt, }, - regs: { - coppa: bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined), - gpp: bidderRequest.ortb2?.regs?.gpp, - gpp_sid: bidderRequest.ortb2?.regs?.gpp_sid - }, + regs: regs, slots: bidRequests.map(bidRequest => { if (!userIdAsEids) { userIdAsEids = bidRequest.userIdAsEids; @@ -444,6 +492,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { networkId = bidRequest.params.networkId || networkId; schain = bidRequest.schain || schain; const slot = { + slotid: bidRequest.bidId, impid: bidRequest.adUnitCode, transactionid: bidRequest.ortb2Imp?.ext?.tid }; @@ -483,6 +532,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (hasVideoMediaType(bidRequest)) { const video = { + context: bidRequest.mediaTypes.video.context, playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), mimes: bidRequest.mediaTypes.video.mimes, protocols: bidRequest.mediaTypes.video.protocols, @@ -493,7 +543,19 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { minduration: bidRequest.mediaTypes.video.minduration, playbackmethod: bidRequest.mediaTypes.video.playbackmethod, startdelay: bidRequest.mediaTypes.video.startdelay, - plcmt: bidRequest.mediaTypes.video.plcmt + plcmt: bidRequest.mediaTypes.video.plcmt, + w: bidRequest.mediaTypes.video.w, + h: bidRequest.mediaTypes.video.h, + linearity: bidRequest.mediaTypes.video.linearity, + skipmin: bidRequest.mediaTypes.video.skipmin, + skipafter: bidRequest.mediaTypes.video.skipafter, + minbitrate: bidRequest.mediaTypes.video.minbitrate, + maxbitrate: bidRequest.mediaTypes.video.maxbitrate, + delivery: bidRequest.mediaTypes.video.delivery, + pos: bidRequest.mediaTypes.video.pos, + playbackend: bidRequest.mediaTypes.video.playbackend, + adPodDurationSec: bidRequest.mediaTypes.video.adPodDurationSec, + durationRangeSec: bidRequest.mediaTypes.video.durationRangeSec, }; const paramsVideo = bidRequest.params.video; if (paramsVideo !== undefined) { @@ -509,6 +571,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { enrichSlotWithFloors(slot, bidRequest); + if (!bidderRequest.fledgeEnabled && slot.ext?.ae) { + delete slot.ext.ae; + } + return slot; }), }; @@ -527,6 +593,8 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { }; request.user = bidderRequest.ortb2?.user || {}; request.site = bidderRequest.ortb2?.site || {}; + request.app = bidderRequest.ortb2?.app || {}; + request.device = bidderRequest.ortb2?.device || {}; if (bidderRequest && bidderRequest.ceh) { request.user.ceh = bidderRequest.ceh; } @@ -601,17 +669,7 @@ function hasValidVideoMediaType(bidRequest) { } }); - if (isValid) { - const videoPlacement = bidRequest.mediaTypes.video.placement || bidRequest.params.video.placement; - // We do not support long form for now, also we have to check that context & placement are consistent - if (bidRequest.mediaTypes.video.context == 'instream' && videoPlacement === 1) { - return true; - } else if (bidRequest.mediaTypes.video.context == 'outstream' && videoPlacement !== 1) { - return true; - } - } - - return false; + return isValid; } /** @@ -768,6 +826,27 @@ function createOutstreamVideoRenderer(slot) { return renderer; } +function getAssociatedBidRequest(bidRequests, slot) { + for (const request of bidRequests) { + if (request.adUnitCode === slot.impid) { + if (request.params.zoneId && parseInt(request.params.zoneId) === slot.zoneid) { + return request; + } else if (slot.native) { + if (request.mediaTypes?.native || request.nativeParams) { + return request; + } + } else if (slot.video) { + if (request.mediaTypes?.video) { + return request; + } + } else if (request.mediaTypes?.banner || request.sizes) { + return request; + } + } + } + return undefined; +} + export function tryGetCriteoFastBid() { // begin ref#1 try { diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index ee343d9b16a..0c42858a0fb 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -13,6 +13,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const gvlid = 91; const bidderCode = 'criteo'; export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: bidderCode }); @@ -22,6 +28,9 @@ const bundleStorageKey = 'cto_bundle'; const dnaBundleStorageKey = 'cto_dna_bundle'; const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000; +const STORAGE_TYPE_LOCALSTORAGE = 'html5'; +const STORAGE_TYPE_COOKIES = 'cookie'; + const pastDateString = new Date(0).toString(); const expirationString = new Date(timestamp() + cookiesMaxAge).toString(); @@ -32,14 +41,26 @@ function extractProtocolHost(url, returnOnlyHost = false) { : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; } -function getFromAllStorages(key) { +function getFromStorage(submoduleConfig, key) { + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + return storage.getDataFromLocalStorage(key); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + return storage.getCookie(key); + } + return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } -function saveOnAllStorages(key, value, hostname) { +function saveOnStorage(submoduleConfig, key, value, hostname) { if (key && value) { - storage.setDataInLocalStorage(key, value); - setCookieOnAllDomains(key, value, expirationString, hostname, true); + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } else { + storage.setDataInLocalStorage(key, value); + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } } } @@ -70,11 +91,11 @@ function deleteFromAllStorages(key, hostname) { storage.removeDataFromLocalStorage(key); } -function getCriteoDataFromAllStorages() { +function getCriteoDataFromStorage(submoduleConfig) { return { - bundle: getFromAllStorages(bundleStorageKey), - dnaBundle: getFromAllStorages(dnaBundleStorageKey), - bidId: getFromAllStorages(bididStorageKey), + bundle: getFromStorage(submoduleConfig, bundleStorageKey), + dnaBundle: getFromStorage(submoduleConfig, dnaBundleStorageKey), + bidId: getFromStorage(submoduleConfig, bididStorageKey), } } @@ -108,7 +129,7 @@ function buildCriteoUsersyncUrl(topUrl, domain, bundle, dnaBundle, areCookiesWri return url; } -function callSyncPixel(domain, pixel) { +function callSyncPixel(submoduleConfig, domain, pixel) { if (pixel.writeBundleInStorage && pixel.bundlePropertyName && pixel.storageKeyName) { ajax( pixel.pixelUrl, @@ -117,7 +138,7 @@ function callSyncPixel(domain, pixel) { if (response) { const jsonResponse = JSON.parse(response); if (jsonResponse && jsonResponse[pixel.bundlePropertyName]) { - saveOnAllStorages(pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); + saveOnStorage(submoduleConfig, pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); } } }, @@ -133,9 +154,9 @@ function callSyncPixel(domain, pixel) { } } -function callCriteoUserSync(parsedCriteoData, callback) { - const cw = storage.cookiesAreEnabled(); - const lsw = storage.localStorageIsEnabled(); +function callCriteoUserSync(submoduleConfig, parsedCriteoData, callback) { + const cw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) && storage.cookiesAreEnabled(); + const lsw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) && storage.localStorageIsEnabled(); const topUrl = extractProtocolHost(getRefererInfo().page); // TODO: should domain really be extracted from the current frame? const domain = extractProtocolHost(document.location.href, true); @@ -156,18 +177,18 @@ function callCriteoUserSync(parsedCriteoData, callback) { const jsonResponse = JSON.parse(response); if (jsonResponse.pixels) { - jsonResponse.pixels.forEach(pixel => callSyncPixel(domain, pixel)); + jsonResponse.pixels.forEach(pixel => callSyncPixel(submoduleConfig, domain, pixel)); } if (jsonResponse.acwsUrl) { const urlsToCall = typeof jsonResponse.acwsUrl === 'string' ? [jsonResponse.acwsUrl] : jsonResponse.acwsUrl; urlsToCall.forEach(url => triggerPixel(url)); } else if (jsonResponse.bundle) { - saveOnAllStorages(bundleStorageKey, jsonResponse.bundle, domain); + saveOnStorage(submoduleConfig, bundleStorageKey, jsonResponse.bundle, domain); } if (jsonResponse.bidId) { - saveOnAllStorages(bididStorageKey, jsonResponse.bidId, domain); + saveOnStorage(submoduleConfig, bididStorageKey, jsonResponse.bidId, domain); const criteoId = { criteoId: jsonResponse.bidId }; callback(criteoId); } else { @@ -207,10 +228,10 @@ export const criteoIdSubmodule = { * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId() { - let localData = getCriteoDataFromAllStorages(); + getId(submoduleConfig) { + let localData = getCriteoDataFromStorage(submoduleConfig); - const result = (callback) => callCriteoUserSync(localData, callback); + const result = (callback) => callCriteoUserSync(submoduleConfig, localData, callback); return { id: localData.bidId ? { criteoId: localData.bidId } : undefined, diff --git a/modules/currency.js b/modules/currency.js index 3da0cfe73e8..eaed4c50df2 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -7,29 +7,24 @@ import {getHook} from '../src/hook.js'; import {defer} from '../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; +import {on as onEvent, off as offEvent} from '../src/events.js'; const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$'; const CURRENCY_RATE_PRECISION = 4; -var bidResponseQueue = []; -var conversionCache = {}; -var currencyRatesLoaded = false; -var needToCallForCurrencyFile = true; -var adServerCurrency = 'USD'; +let ratesURL; +let bidResponseQueue = []; +let conversionCache = {}; +let currencyRatesLoaded = false; +let needToCallForCurrencyFile = true; +let adServerCurrency = 'USD'; export var currencySupportEnabled = false; export var currencyRates = {}; -var bidderCurrencyDefault = {}; -var defaultRates; +let bidderCurrencyDefault = {}; +let defaultRates; -export const ready = (() => { - let ctl; - function reset() { - ctl = defer(); - } - reset(); - return {done: () => ctl.resolve(), reset, promise: () => ctl.promise} -})(); +export let responseReady = defer(); /** * Configuration function for currency @@ -64,7 +59,7 @@ export const ready = (() => { * there is an error loading the config.conversionRateFile. */ export function setConfig(config) { - let url = DEFAULT_CURRENCY_RATE_URL; + ratesURL = DEFAULT_CURRENCY_RATE_URL; if (typeof config.rates === 'object') { currencyRates.conversions = config.rates; @@ -86,14 +81,14 @@ export function setConfig(config) { adServerCurrency = config.adServerCurrency; if (config.conversionRateFile) { logInfo('currency using override conversionRateFile:', config.conversionRateFile); - url = config.conversionRateFile; + ratesURL = config.conversionRateFile; } // see if the url contains a date macro // this is a workaround to the fact that jsdelivr doesn't currently support setting a 24-hour HTTP cache header // So this is an approach to let the browser cache a copy of the file each day // We should remove the macro once the CDN support a day-level HTTP cache setting - const macroLocation = url.indexOf('$$TODAY$$'); + const macroLocation = ratesURL.indexOf('$$TODAY$$'); if (macroLocation !== -1) { // get the date to resolve the macro const d = new Date(); @@ -104,10 +99,10 @@ export function setConfig(config) { const todaysDate = `${d.getFullYear()}${month}${day}`; // replace $$TODAY$$ with todaysDate - url = `${url.substring(0, macroLocation)}${todaysDate}${url.substring(macroLocation + 9, url.length)}`; + ratesURL = `${ratesURL.substring(0, macroLocation)}${todaysDate}${ratesURL.substring(macroLocation + 9, ratesURL.length)}`; } - initCurrency(url); + initCurrency(); } else { // currency support is disabled, setting defaults logInfo('disabling currency support'); @@ -128,20 +123,11 @@ function errorSettingsRates(msg) { } } -function initCurrency(url) { - conversionCache = {}; - currencySupportEnabled = true; - - logInfo('Installing addBidResponse decorator for currency module', arguments); - - // Adding conversion function to prebid global for external module and on page use - getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); - getHook('addBidResponse').before(addBidResponseHook, 100); - - // call for the file if we haven't already +function loadRates() { if (needToCallForCurrencyFile) { needToCallForCurrencyFile = false; - ajax(url, + currencyRatesLoaded = false; + ajax(ratesURL, { success: function (response) { try { @@ -150,26 +136,45 @@ function initCurrency(url) { conversionCache = {}; currencyRatesLoaded = true; processBidResponseQueue(); - ready.done(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } }, error: function (...args) { errorSettingsRates(...args); - ready.done(); + currencyRatesLoaded = true; + processBidResponseQueue(); + needToCallForCurrencyFile = true; } } ); } else { - ready.done(); + processBidResponseQueue(); } } +function initCurrency() { + conversionCache = {}; + currencySupportEnabled = true; + + logInfo('Installing addBidResponse decorator for currency module', arguments); + + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); + getHook('addBidResponse').before(addBidResponseHook, 100); + getHook('responsesReady').before(responsesReadyHook); + onEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + onEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); + loadRates(); +} + function resetCurrency() { logInfo('Uninstalling addBidResponse decorator for currency module', arguments); getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); + offEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + offEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); delete getGlobal().convertCurrency; adServerCurrency = 'USD'; @@ -179,6 +184,11 @@ function resetCurrency() { needToCallForCurrencyFile = true; currencyRates = {}; bidderCurrencyDefault = {}; + responseReady = defer(); +} + +function responsesReadyHook(next, ready) { + next(ready.then(() => responseReady.promise)); } export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid, reject) { @@ -211,24 +221,25 @@ export const addBidResponseHook = timedBidResponseHook('currency', function addB if (bid.currency === adServerCurrency) { return fn.call(this, adUnitCode, bid, reject); } - - bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid, reject])); + bidResponseQueue.push([fn, this, adUnitCode, bid, reject]); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); - } else { - fn.untimed.bail(ready.promise()); } }); -function processBidResponseQueue() { - while (bidResponseQueue.length > 0) { - (bidResponseQueue.shift())(); - } +function rejectOnAuctionTimeout({auctionId}) { + bidResponseQueue = bidResponseQueue.filter(([fn, ctx, adUnitCode, bid, reject]) => { + if (bid.auctionId === auctionId) { + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY) + } else { + return true; + } + }); } -function wrapFunction(fn, context, params) { - return function() { - let bid = params[1]; +function processBidResponseQueue() { + while (bidResponseQueue.length > 0) { + const [fn, ctx, adUnitCode, bid, reject] = bidResponseQueue.shift(); if (bid !== undefined && 'currency' in bid && 'cpm' in bid) { let fromCurrency = bid.currency; try { @@ -239,12 +250,13 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('getCurrencyConversion threw error: ', e); - params[2](CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); - return; + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + continue; } } - return fn.apply(context, params); - }; + fn.call(ctx, adUnitCode, bid, reject); + } + responseReady.resolve(); } function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) { diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 604d7235d0f..f878be5f66a 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -2,6 +2,13 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {generateUUID, getParameterByName, isNumber, logError, logInfo} from '../src/utils.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ // ------------------------------------ const BIDDER_CODE = 'cwire'; @@ -9,6 +16,7 @@ const CWID_KEY = 'cw_cwid'; export const BID_ENDPOINT = 'https://prebid.cwi.re/v1/bid'; export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v1/event'; +export const GVL_ID = 1081; /** * Allows limiting ad impressions per site render. Unique per prebid instance ID. @@ -133,6 +141,7 @@ function getCwExtension() { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER], /** @@ -230,5 +239,23 @@ export const spec = { navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + logInfo('Collecting user-syncs: ', JSON.stringify({syncOptions, gdprConsent, uspConsent, serverResponses})); + + const syncs = [] + if (hasPurpose1Consent(gdprConsent)) { + logInfo('GDPR purpose 1 consent was given, adding user-syncs') + let type = (syncOptions.pixelEnabled) ? 'image' : null ?? (syncOptions.iframeEnabled) ? 'iframe' : null + if (type) { + syncs.push({ + type: type, + url: 'https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID' + }) + } + } + logInfo('Collected user-syncs: ', JSON.stringify({syncs})) + return syncs + } + }; registerBidder(spec); diff --git a/modules/czechAdIdSystem.js b/modules/czechAdIdSystem.js index ae958aae198..7fdf462183a 100644 --- a/modules/czechAdIdSystem.js +++ b/modules/czechAdIdSystem.js @@ -9,6 +9,11 @@ import { submodule } from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + // Returns StorageManager export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: 'czechAdId' }) diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 11d3ebb1589..395706994fe 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,10 +1,11 @@ -import {deepAccess, getAdUnitSizes, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; +import {deepAccess, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; export const storage = getStorageManager({bidderCode: 'datablocks'}); diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index 2cf28c36330..db795c89155 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -1,4 +1,12 @@ -import { deepAccess, getBidIdParameter, isArray, getUniqueIdentifierStr, contains, isFn, isPlainObject } from '../src/utils.js'; +import { + deepAccess, + isArray, + getUniqueIdentifierStr, + contains, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -7,6 +15,11 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import CONSTANTS from '../src/constants.json'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'datawrkz'; const ALIASES = []; const ENDPOINT_URL = 'https://at.datawrkz.com/exchange/openrtb23/'; diff --git a/modules/dchain.js b/modules/dchain.js index daf97a7551f..7f84282b81e 100644 --- a/modules/dchain.js +++ b/modules/dchain.js @@ -1,7 +1,7 @@ import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; -import {_each, deepAccess, deepClone, hasOwn, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; +import {_each, deepAccess, deepClone, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; const shouldBeAString = ' should be a string'; @@ -49,7 +49,7 @@ export function checkDchainSyntax(bid, mode) { appendFailMsg(`dchain.ver` + shouldBeAString); } - if (hasOwn(dchainObj, 'ext')) { + if (dchainObj.hasOwnProperty('ext')) { if (!isPlainObject(dchainObj.ext)) { appendFailMsg(`dchain.ext` + shouldBeAnObject); } diff --git a/modules/debugging/bidInterceptor.js b/modules/debugging/bidInterceptor.js index 775f8fc3da2..3afaacaeb81 100644 --- a/modules/debugging/bidInterceptor.js +++ b/modules/debugging/bidInterceptor.js @@ -54,8 +54,9 @@ Object.assign(BidInterceptor.prototype, { return { no: ruleNo, match: this.matcher(ruleDef.when, ruleNo), - replace: this.replacer(ruleDef.then || {}, ruleNo), + replace: this.replacer(ruleDef.then, ruleNo), options: Object.assign({}, this.DEFAULT_RULE_OPTIONS, ruleDef.options), + paapi: this.paapiReplacer(ruleDef.paapi || [], ruleNo) } }, /** @@ -114,6 +115,10 @@ Object.assign(BidInterceptor.prototype, { * @return {ReplacerFn} */ replacer(replDef, ruleNo) { + if (replDef === null) { + return () => null + } + replDef = replDef || {}; let replFn; if (typeof replDef === 'function') { replFn = ({args}) => replDef(...args); @@ -145,6 +150,17 @@ Object.assign(BidInterceptor.prototype, { return response; } }, + + paapiReplacer(paapiDef, ruleNo) { + if (Array.isArray(paapiDef)) { + return () => paapiDef; + } else if (typeof paapiDef === 'function') { + return paapiDef + } else { + this.logger.logError(`Invalid 'paapi' definition for debug bid interceptor (in rule #${ruleNo})`); + } + }, + responseDefaults(bid) { return { requestId: bid.bidId, @@ -198,11 +214,12 @@ Object.assign(BidInterceptor.prototype, { * @param {{}[]} bids? * @param {BidRequest} bidRequest * @param {function(*)} addBid called once for each mock response + * @param addPaapiConfig called once for each mock PAAPI config * @param {function()} done called once after all mock responses have been run through `addBid` * @returns {{bids: {}[], bidRequest: {}} remaining bids that did not match any rule (this applies also to * bidRequest.bids) */ - intercept({bids, bidRequest, addBid, done}) { + intercept({bids, bidRequest, addBid, addPaapiConfig, done}) { if (bids == null) { bids = bidRequest.bids; } @@ -211,10 +228,12 @@ Object.assign(BidInterceptor.prototype, { const callDone = delayExecution(done, matches.length); matches.forEach((match) => { const mockResponse = match.rule.replace(match.bid, bidRequest); + const mockPaapi = match.rule.paapi(match.bid, bidRequest); const delay = match.rule.options.delay; - this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse) + this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response, PAAPI configs:`, match.bid, mockResponse, mockPaapi) this.setTimeout(() => { - addBid(mockResponse, match.bid); + mockResponse && addBid(mockResponse, match.bid); + mockPaapi.forEach(cfg => addPaapiConfig(cfg, match.bid, bidRequest)); callDone(); }, delay) }); diff --git a/modules/debugging/debugging.js b/modules/debugging/debugging.js index 8a4ad7a9545..2fd1731dc4e 100644 --- a/modules/debugging/debugging.js +++ b/modules/debugging/debugging.js @@ -99,7 +99,13 @@ function registerBidInterceptor(getHookFn, interceptor) { export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) { const done = delayExecution(cbs.onCompletion, 2); - ({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done})); + ({bids, bidRequest} = interceptBids({ + bids, + bidRequest, + addBid: cbs.onBid, + addPaapiConfig: (config, bidRequest) => cbs.onPaapi({bidId: bidRequest.bidId, config}), + done + })); if (bids.length === 0) { done(); } else { diff --git a/modules/debugging/pbsInterceptor.js b/modules/debugging/pbsInterceptor.js index 1ca13eb4927..73df01bf205 100644 --- a/modules/debugging/pbsInterceptor.js +++ b/modules/debugging/pbsInterceptor.js @@ -5,7 +5,8 @@ export function makePbsInterceptor({createBid}) { return function pbsBidInterceptor(next, interceptBids, s2sBidRequest, bidRequests, ajax, { onResponse, onError, - onBid + onBid, + onFledge, }) { let responseArgs; const done = delayExecution(() => onResponse(...responseArgs), bidRequests.length + 1) @@ -20,7 +21,19 @@ export function makePbsInterceptor({createBid}) { }) } bidRequests = bidRequests - .map((req) => interceptBids({bidRequest: req, addBid, done}).bidRequest) + .map((req) => interceptBids({ + bidRequest: req, + addBid, + addPaapiConfig(config, bidRequest, bidderRequest) { + onFledge({ + adUnitCode: bidRequest.adUnitCode, + ortb2: bidderRequest.ortb2, + ortb2Imp: bidRequest.ortb2Imp, + config + }) + }, + done + }).bidRequest) .filter((req) => req.bids.length > 0) if (bidRequests.length > 0) { diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index e062686b320..0a64ed88ca5 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -2,6 +2,7 @@ import { generateUUID, deepSetValue, deepAccess, isArray, isInteger, logError, l import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'deepintent'; +const GVL_ID = 541; const BIDDER_ENDPOINT = 'https://prebid.deepintent.com/prebid'; const USER_SYNC_URL = 'https://cdn.deepintent.com/syncpixel.html'; const DI_M_V = '1.0.0'; @@ -32,6 +33,7 @@ export const ORTB_VIDEO_PARAMS = { }; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: [], @@ -96,6 +98,20 @@ export const spec = { deepSetValue(openRtbBidRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); } + // GPP Consent + if (bidderRequest?.gppConsent?.gppString) { + deepSetValue(openRtbBidRequest, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(openRtbBidRequest, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (bidderRequest?.ortb2?.regs?.gpp) { + deepSetValue(openRtbBidRequest, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(openRtbBidRequest, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } + + // coppa compliance + if (bidderRequest?.ortb2?.regs?.coppa) { + deepSetValue(openRtbBidRequest, 'regs.coppa', 1); + } + injectEids(openRtbBidRequest, validBidRequests); return { diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 4d685592c04..2d3eae980cd 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -9,6 +9,12 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const MODULE_NAME = 'deepintentId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/deltaprojectsBidAdapter.js b/modules/deltaprojectsBidAdapter.js index c66e381b8f1..870378a13dd 100644 --- a/modules/deltaprojectsBidAdapter.js +++ b/modules/deltaprojectsBidAdapter.js @@ -16,7 +16,7 @@ export const BIDDER_CODE = 'deltaprojects'; export const BIDDER_ENDPOINT_URL = 'https://d5p.de17a.com/dogfight/prebid'; export const USERSYNC_URL = 'https://userservice.de17a.com/getuid/prebid'; -/** -- isBidRequestValid --**/ +/** -- isBidRequestValid -- */ function isBidRequestValid(bid) { if (!bid) return false; @@ -32,9 +32,9 @@ function isBidRequestValid(bid) { return true; } -/** -- Build requests --**/ +/** -- Build requests -- */ function buildRequests(validBidRequests, bidderRequest) { - /** == shared ==**/ + /** == shared == */ // -- build id const id = bidderRequest.bidderRequestId; @@ -146,7 +146,7 @@ function buildImpressionBanner(bid, bannerMediaType) { }; } -/** -- Interpret response --**/ +/** -- Interpret response -- */ function interpretResponse(serverResponse) { if (!serverResponse.body) { logWarn('Response body is invalid, return !!'); @@ -189,7 +189,7 @@ function interpretResponse(serverResponse) { return bidResponses; } -/** -- On Bid Won -- **/ +/** -- On Bid Won -- */ function onBidWon(bid) { let cpm = bid.cpm; if (bid.currency && bid.currency !== bid.originalCurrency && typeof bid.getCpmInNewCurrency === 'function') { @@ -200,7 +200,7 @@ function onBidWon(bid) { bid.ad = bid.ad.replace(wonPriceMacroPatten, wonPrice); } -/** -- Get user syncs --**/ +/** -- Get user syncs -- */ function getUserSyncs(syncOptions, serverResponses, gdprConsent) { const syncs = [] @@ -223,7 +223,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent) { return syncs; } -/** -- Get bid floor --**/ +/** -- Get bid floor -- */ export function getBidFloor(bid, mediaType, size, currency) { if (isFn(bid.getFloor)) { const bidFloorCurrency = currency || 'USD'; @@ -234,7 +234,7 @@ export function getBidFloor(bid, mediaType, size, currency) { } } -/** -- Helper methods --**/ +/** -- Helper methods -- */ function setOnAny(collection, key) { for (let i = 0, result; i < collection.length; i++) { result = deepAccess(collection[i], key); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 3394fd8b3f4..7f275992210 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -2,17 +2,28 @@ * This module adds [DFP support]{@link https://www.doubleclickbygoogle.com/} for Video to Prebid. */ -import { registerVideoSupport } from '../src/adServerManager.js'; -import { targeting } from '../src/targeting.js'; -import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, buildUrl } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { getHook, submodule } from '../src/hook.js'; -import { auctionManager } from '../src/auctionManager.js'; -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +import {registerVideoSupport} from '../src/adServerManager.js'; +import {targeting} from '../src/targeting.js'; +import { + isNumber, + buildUrl, + deepAccess, + formatQS, + isEmpty, + logError, + parseSizesInput, + parseUrl, + uniques +} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {getHook, submodule} from '../src/hook.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {gdprDataHandler} from '../src/adapterManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {getPPID} from '../src/adserver.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import {CLIENT_SECTIONS} from '../src/fpd/oneClient.js'; /** * @typedef {Object} DfpVideoParams @@ -113,7 +124,6 @@ export function buildDfpVideoUrl(options) { const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } - const gdprConsent = gdprDataHandler.getConsentData(); if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } @@ -121,14 +131,6 @@ export function buildDfpVideoUrl(options) { if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } } - const uspConsent = uspDataHandler.getConsentData(); - if (uspConsent) { queryParams.us_privacy = uspConsent; } - - const gppConsent = gppDataHandler.getConsentData(); - if (gppConsent) { - // TODO - need to know what to set here for queryParams... - } - if (!queryParams.ppid) { const ppid = getPPID(); if (ppid != null) { @@ -136,6 +138,70 @@ export function buildDfpVideoUrl(options) { } } + const video = options.adUnit?.mediaTypes?.video; + Object.entries({ + plcmt: () => video?.plcmt, + min_ad_duration: () => isNumber(video?.minduration) ? video.minduration * 1000 : null, + max_ad_duration: () => isNumber(video?.maxduration) ? video.maxduration * 1000 : null, + vpos() { + const startdelay = video?.startdelay; + if (isNumber(startdelay)) { + if (startdelay === -2) return 'postroll'; + if (startdelay === -1 || startdelay > 0) return 'midroll'; + return 'preroll'; + } + }, + vconp: () => Array.isArray(video?.playbackmethod) && video.playbackmethod.every(m => m === 7) ? '2' : undefined, + vpa() { + // playbackmethod = 3 is play on click; 1, 2, 4, 5, 6 are autoplay + if (Array.isArray(video?.playbackmethod)) { + const click = video.playbackmethod.some(m => m === 3); + const auto = video.playbackmethod.some(m => [1, 2, 4, 5, 6].includes(m)); + if (click && !auto) return 'click'; + if (auto && !click) return 'auto'; + } + }, + vpmute() { + // playbackmethod = 2, 6 are muted; 1, 3, 4, 5 are not + if (Array.isArray(video?.playbackmethod)) { + const muted = video.playbackmethod.some(m => [2, 6].includes(m)); + const talkie = video.playbackmethod.some(m => [1, 3, 4, 5].includes(m)); + if (muted && !talkie) return '1'; + if (talkie && !muted) return '0'; + } + } + }).forEach(([param, getter]) => { + if (!queryParams.hasOwnProperty(param)) { + const val = getter(); + if (val != null) { + queryParams[param] = val; + } + } + }); + const fpd = auctionManager.index.getBidRequest(options.bid || {})?.ortb2 ?? + auctionManager.index.getAuction(options.bid || {})?.getFPD()?.global; + + function getSegments(sections, segtax) { + return sections + .flatMap(section => deepAccess(fpd, section) || []) + .filter(datum => datum.ext?.segtax === segtax) + .flatMap(datum => datum.segment?.map(seg => seg.id)) + .filter(ob => ob) + .filter(uniques) + } + + const signals = Object.entries({ + IAB_AUDIENCE_1_1: getSegments(['user.data'], 4), + IAB_CONTENT_2_2: getSegments(CLIENT_SECTIONS.map(section => `${section}.content.data`), 6) + }).map(([taxonomy, values]) => values.length ? {taxonomy, values} : null) + .filter(ob => ob); + + if (signals.length) { + queryParams.ppsj = btoa(JSON.stringify({ + PublisherProvidedTaxonomySignals: signals + })) + } + return buildUrl(Object.assign({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -164,6 +230,8 @@ if (config.getConfig('brandCategoryTranslation.translationFile')) { getHook('reg * @returns {string} A URL which calls DFP with custom adpod targeting key values to compete with rest of the demand in DFP */ export function buildAdpodVideoUrl({code, params, callback} = {}) { + // TODO: the public API for this does not take in enough info to fill all DFP params (adUnit/bid), + // and is marked "alpha": https://docs.prebid.org/dev-docs/publisher-api-reference/adServers.dfp.buildAdpodVideoUrl.html if (!params || !callback) { logError(`A params object and a callback is required to use pbjs.adServers.dfp.buildAdpodVideoUrl`); return; @@ -225,9 +293,6 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } } - const uspConsent = uspDataHandler.getConsentData(); - if (uspConsent) { queryParams.us_privacy = uspConsent; } - const masterTag = buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -249,7 +314,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { */ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); - if (descriptionUrl) { components.search.description_url = descriptionUrl; } + if (descriptionUrl) { + components.search.description_url = descriptionUrl; + } components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); @@ -264,7 +331,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { * @return {string | undefined} The encoded vast url if it exists, or undefined */ function getDescriptionUrl(bid, components, prop) { - return deepAccess(components, `${prop}.description_url`) || dep.ri().page; + return deepAccess(components, `${prop}.description_url`) || encodeURIComponent(dep.ri().page); } /** diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index 99df3b18a39..14519ae2713 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -6,12 +6,15 @@ * @module modules/dgkeywordProvider * @requires module:modules/realTimeData */ - -import {logMessage, deepSetValue, logError, logInfo, mergeDeep} from '../src/utils.js'; +import { logMessage, deepSetValue, logError, logInfo, isStr, isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getGlobal } from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** * get keywords from api server. and set keywords. * @param {Object} reqBidsConfigObj @@ -57,20 +60,12 @@ export function getDgKeywordsAndSet(reqBidsConfigObj, callback, moduleConfig, us if (Object.keys(keywords).length > 0) { const targetBidKeys = {}; for (let bid of setKeywordTargetBidders) { - // set keywords to params - bid.params.keywords = keywords; + // set keywords to ortb2Imp + deepSetValue(bid, 'ortb2Imp.ext.data.keywords', convertKeywordsToString(keywords)); if (!targetBidKeys[bid.bidder]) { targetBidKeys[bid.bidder] = true; } } - - if (!reqBidsConfigObj._ignoreSetOrtb2) { - // set keywrods to ortb2 - let addOrtb2 = {}; - deepSetValue(addOrtb2, 'site.keywords', keywords); - deepSetValue(addOrtb2, 'user.keywords', keywords); - mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, Object.fromEntries(Object.keys(targetBidKeys).map(bidder => [bidder, addOrtb2]))); - } } } isFinish = true; @@ -156,4 +151,37 @@ function init(moduleConfig) { function registerSubModule() { submodule('realTimeData', dgkeywordSubmodule); } + +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +export function convertKeywordsToString(keywords) { + let result = ''; + Object.keys(keywords).forEach(key => { + // if 'text' or '' + if (isStr(keywords[key])) { + if (keywords[key] !== '') { + result += `${key}=${keywords[key]},` + } else { + result += `${key},`; + } + } else if (isArray(keywords[key])) { + let isValSet = false + keywords[key].forEach(val => { + if (isStr(val) && val) { + result += `${key}=${val},` + isValSet = true + } + }); + if (!isValSet) { + result += `${key},` + } + } else { + result += `${key},` + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + registerSubModule(); diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 7ad75f64215..7493dcb9af4 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -3,17 +3,30 @@ import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'discovery'; const ENDPOINT_URL = 'https://rtb-jp.mediago.io/api/bid?tn='; const TIME_TO_LIVE = 500; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; const MEDIATYPE = [BANNER, NATIVE]; /* ----- _ss_pp_id:start ------ */ const COOKIE_KEY_SSPPID = '_ss_pp_id'; -const COOKIE_KEY_MGUID = '__mguid_'; +export const COOKIE_KEY_MGUID = '__mguid_'; +const COOKIE_KEY_PMGUID = '__pmguid_'; +const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year +const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html'; +export const THIRD_PARTY_COOKIE_ORIGIN = 'https://asset.popin.cc'; + +const UTM_KEY = '_ss_pp_utm'; +let UTMValue = {}; const NATIVERET = { id: 'id', @@ -55,24 +68,80 @@ const NATIVERET = { }; /** - * čŽˇå–į”¨æˆˇid + * get page title111 + * @returns {string} + */ + +export function getPageTitle(win = window) { + try { + const ogTitle = win.top.document.querySelector('meta[property="og:title"]') + return win.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +/** + * get page description + * @returns {string} + */ +export function getPageDescription(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="description"]') || + win.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +/** + * get page keywords + * @returns {string} + */ +export function getPageKeywords(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); + } + + return (element && element.content) || ''; +} + +/** + * get connection downlink + * @returns {number} + */ +export function getConnectionDownLink(win = window) { + const nav = win.navigator || {}; + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; +} + +/** + * get pmg uid + * čŽˇå–åšļį”Ÿæˆį”¨æˆˇįš„id * @return {string} */ -const getUserID = () => { - let idd = storage.getCookie(COOKIE_KEY_SSPPID); - let idm = storage.getCookie(COOKIE_KEY_MGUID); - - if (idd && !idm) { - idm = idd; - } else if (idm && !idd) { - idd = idm; - } else if (!idd && !idm) { - const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid); - storage.setCookie(COOKIE_KEY_SSPPID, uuid); - return uuid; +export const getPmgUID = () => { + if (!storage.cookiesAreEnabled()) return; + + let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID); + if (!pmgUid) { + pmgUid = utils.generateUUID(); } - return idd; + // Extend the expiration time of pmguid + try { + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString()); + } catch (e) {} + return pmgUid; }; /* ----- _ss_pp_id:end ------ */ @@ -211,6 +280,74 @@ const popInAdSize = [ { w: 336, h: 280 }, ]; +/** + * get screen size + * + * @returns {Array} eg: "['widthxheight']" + */ +function getScreenSize() { + return utils.parseSizesInput([window.screen.width, window.screen.height]); +} + +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + +/** + * get current time to UTC string + * @returns utc string + */ +export function getCurrentTimeToUTCString() { + const date = new Date(); + date.setTime(date.getTime() + COOKIE_RETENTION_TIME); + return date.toUTCString(); +} + +/** + * format imp ad test ext params + * + * @param validBidRequest sigleBidRequest + * @param bidderRequest + */ +function addImpExtParams(bidRequest = {}, bidderRequest = {}) { + const { deepAccess } = utils; + const { params = {}, adUnitCode, bidId } = bidRequest; + const ext = { + bidId: bidId || '', + adUnitCode: adUnitCode || '', + token: params.token || '', + siteId: params.siteId || '', + zoneId: params.zoneId || '', + publisher: params.publisher || '', + p_pos: params.position || '', + screenSize: getScreenSize(), + referrer: getReferrer(bidRequest, bidderRequest), + stack: deepAccess(bidRequest, 'refererInfo.stack', []), + b_pos: deepAccess(bidRequest, 'mediaTypes.banner.pos', '', ''), + ortbUser: deepAccess(bidRequest, 'ortb2.user', {}, {}), + ortbSite: deepAccess(bidRequest, 'ortb2.site', {}, {}), + tid: deepAccess(bidRequest, 'ortb2Imp.ext.tid', '', ''), + browsiViewability: deepAccess(bidRequest, 'ortb2Imp.ext.data.browsi.browsiViewability', '', ''), + adserverName: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name', '', ''), + adslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + keywords: deepAccess(bidRequest, 'ortb2Imp.ext.data.keywords', '', ''), + gpid: deepAccess(bidRequest, 'ortb2Imp.ext.gpid', '', ''), + pbadslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot', '', ''), + }; + return ext; +} + /** * get aditem setting * @param {Array} validBidRequests an an array of bids @@ -261,6 +398,11 @@ function getItems(validBidRequests, bidderRequest) { tagid: req.params && req.params.tagid }; } + + try { + ret.ext = addImpExtParams(req, bidderRequest); + } catch (e) {} + itemMaps[id] = { req, ret, @@ -270,6 +412,20 @@ function getItems(validBidRequests, bidderRequest) { return items; } +export const buildUTMTagData = (url) => { + if (!storage.cookiesAreEnabled()) return; + const urlParams = utils.parseUrl(url).search; + const UTMParams = {}; + Object.keys(urlParams).forEach(key => { + if (/^utm_/.test(key)) { + UTMParams[key] = urlParams[key]; + } + }); + UTMValue = JSON.parse(storage.getCookie(UTM_KEY) || '{}'); + Object.assign(UTMValue, UTMParams); + storage.setCookie(UTM_KEY, JSON.stringify(UTMValue), getCurrentTimeToUTCString()); +} + /** * get rtb qequest params * @@ -298,6 +454,15 @@ function getParam(validBidRequests, bidderRequest) { const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const firstPartyData = bidderRequest.ortb2; + const tpData = utils.deepAccess(bidderRequest, 'ortb2.user.data') || undefined; + const topWindow = window.top; + const title = getPageTitle(); + const desc = getPageDescription(); + const keywords = getPageKeywords(); + + try { + buildUTMTagData(page); + } catch (error) { } if (items && items.length) { let c = { @@ -318,12 +483,25 @@ function getParam(validBidRequests, bidderRequest) { ext: { eids, firstPartyData, + ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined, + pmguid: getPmgUID(), + tpData, + page: { + title: title ? title.slice(0, 100) : undefined, + desc: desc ? desc.slice(0, 300) : undefined, + keywords: keywords ? keywords.slice(0, 100) : undefined, + hLen: topWindow.history?.length || undefined, + }, + device: { + nbw: getConnectionDownLink(), + hc: topWindow.navigator?.hardwareConcurrency || undefined, + dm: topWindow.navigator?.deviceMemory || undefined, + } }, user: { - buyeruid: getUserID(), + buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, id: sharedid || pubcid, }, - eids, tmax: timeout, site: { name: domain, @@ -372,7 +550,7 @@ export const spec = { if (bid.params.badv) { globals['badv'] = Array.isArray(bid.params.badv) ? bid.params.badv : []; } - return !!(bid.params.token && bid.params.publisher && bid.params.tagid); + return true; }, /** @@ -383,6 +561,8 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + if (!globals['token']) return; + let payload = getParam(validBidRequests, bidderRequest); const payloadString = JSON.stringify(payload); @@ -485,6 +665,45 @@ export const spec = { return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + window.addEventListener('message', function handler(event) { + if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + this.removeEventListener('message', handler); + + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + }, true); + return [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + } + }, + /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index 3d34f2c8b26..3cdfd3a77cd 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import {getWindowFromDocument, logWarn} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; @@ -156,7 +156,7 @@ function newRenderer(bid) { function webisRender(bid, doc) { bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.webis.init(bid.adData, bid.adUnitCode, bid.params); }) } diff --git a/modules/dmdIdSystem.js b/modules/dmdIdSystem.js index 2f910a8bd92..3575e658a2a 100644 --- a/modules/dmdIdSystem.js +++ b/modules/dmdIdSystem.js @@ -9,6 +9,13 @@ import { logError, getWindowLocation } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'dmdId'; /** @type {Submodule} */ diff --git a/modules/docereeAdManagerBidAdapter.js b/modules/docereeAdManagerBidAdapter.js new file mode 100644 index 00000000000..d3765f5a130 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.js @@ -0,0 +1,128 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'docereeadmanager'; +const END_POINT = 'https://dai.doceree.com/drs/quest'; + +export const spec = { + code: BIDDER_CODE, + url: '', + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + const { placementId } = bid.params; + return !!placementId; + }, + isGdprConsentPresent: (bid) => { + const { gdpr, gdprconsent } = bid.params; + if (gdpr == '1') { + return !!gdprconsent; + } + return true; + }, + buildRequests: (validBidRequests) => { + const serverRequests = []; + const { data } = config.getConfig('docereeadmanager.user') || {}; + + validBidRequests.forEach(function (validBidRequest) { + const payload = getPayload(validBidRequest, data); + + if (!payload) { + return; + } + + serverRequests.push({ + method: 'POST', + url: END_POINT, + data: JSON.stringify(payload.data), + options: { + contentType: 'application/json', + withCredentials: true, + }, + }); + }); + + return serverRequests; + }, + interpretResponse: (serverResponse) => { + const responseJson = serverResponse ? serverResponse.body : {}; + const bidResponse = { + ad: responseJson.ad, + width: Number(responseJson.width), + height: Number(responseJson.height), + requestId: responseJson.requestId, + netRevenue: true, + ttl: 30, + cpm: responseJson.cpm, + currency: responseJson.currency, + mediaType: BANNER, + creativeId: responseJson.creativeId, + meta: { + advertiserDomains: + Array.isArray(responseJson.meta.advertiserDomains) && + responseJson.meta.advertiserDomains.length > 0 + ? responseJson.meta.advertiserDomains + : [], + }, + }; + + return [bidResponse]; + }, +}; + +function getPayload(bid, userData) { + if (!userData || !bid) { + return false; + } + + const { bidId, params } = bid; + const { placementId } = params; + const { + userid, + email, + firstname, + lastname, + specialization, + hcpid, + gender, + city, + state, + zipcode, + hashedNPI, + hashedhcpid, + hashedemail, + hashedmobile, + country, + organization, + dob, + } = userData; + + const data = { + userid: userid || '', + email: email || '', + firstname: firstname || '', + lastname: lastname || '', + specialization: specialization || '', + hcpid: hcpid || '', + gender: gender || '', + city: city || '', + state: state || '', + zipcode: zipcode || '', + hashedNPI: hashedNPI || '', + pb: 1, + adunit: placementId || '', + requestId: bidId || '', + hashedhcpid: hashedhcpid || '', + hashedemail: hashedemail || '', + hashedmobile: hashedmobile || '', + country: country || '', + organization: organization || '', + dob: dob || '', + userconsent: 1, + }; + return { + data, + }; +} + +registerBidder(spec); diff --git a/modules/docereeAdManagerBidAdapter.md b/modules/docereeAdManagerBidAdapter.md new file mode 100644 index 00000000000..bedbf57b179 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Doceree AdManager Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech.stack@doceree.com +``` + + + +Connects to Doceree demand source to fetch bids. +Please use `docereeadmanager` as the bidder code. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'DOC-397-1', + sizes: [ + [300, 250] + ], + bids: [ + { + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', //required + publisherUrl: document.URL || window.location.href, //optional + gdpr: '1', //optional + gdprconsent:'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', //optional + } + } + ] + } +]; +``` + +```javascript +pbjs.setBidderConfig({ + bidders: ['docereeadmanager'], + config: { + docereeadmanager: { + user: { + data: { + email: 'XXX.XXX@GMAIL.COM', + firstname: 'DR. XXX', + lastname: 'XXX', + mobile: '981234XXXX', + specialization: 'Internal Medicine', + organization: 'Max Lifecare', + hcpid: '199291XXXX', + dob: '1987-08-27', + gender: 'Female', + city: 'Oildale', + state: 'California', + country: 'California', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '7d26d8ca-233a-46c2-9d36-7c5d261e151d', + zipcode: '', + userconsent: '1', + }, + }, + }, + }, +}); +``` diff --git a/modules/docereeBidAdapter.js b/modules/docereeBidAdapter.js index 524f464cee3..2731e1ff397 100644 --- a/modules/docereeBidAdapter.js +++ b/modules/docereeBidAdapter.js @@ -1,9 +1,11 @@ -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel } from '../src/utils.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'doceree'; const END_POINT = 'https://bidder.doceree.com' +const TRACKING_END_POINT = 'https://tracking.doceree.com' export const spec = { code: BIDDER_CODE, @@ -69,6 +71,33 @@ export const spec = { } }; return [bidResponse]; + }, + onTimeout: function(timeoutData) { + if (timeoutData == null || !timeoutData.length) { + return; + } + timeoutData.forEach(td => { + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + bidId: td.bidId, + timeout: td.timeout, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbTimeout?adp=prebidjs&data=' + encodedBuf); + }) + }, + onBidWon: function (bidWon) { + if (bidWon == null) { + return; + } + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + requestId: bidWon.requestId, + cpm: bidWon.cpm, + adId: bidWon.adId, + currency: bidWon.currency, + netRevenue: bidWon.netRevenue, + status: bidWon.status, + hb_pb: bidWon.adserverTargeting && bidWon.adserverTargeting.hb_pb, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbBidWon?adp=prebidjs&data=' + encodedBuf); } }; diff --git a/modules/dsaControl.js b/modules/dsaControl.js new file mode 100644 index 00000000000..b08a6ea1f4e --- /dev/null +++ b/modules/dsaControl.js @@ -0,0 +1,67 @@ +import {config} from '../src/config.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; +import CONSTANTS from '../src/constants.json'; +import {getHook} from '../src/hook.js'; +import {logInfo, logWarn} from '../src/utils.js'; + +let expiryHandle; +let dsaAuctions = {}; + +export const addBidResponseHook = timedBidResponseHook('dsa', function (fn, adUnitCode, bid, reject) { + if (!dsaAuctions.hasOwnProperty(bid.auctionId)) { + dsaAuctions[bid.auctionId] = auctionManager.index.getAuction(bid)?.getFPD?.()?.global?.regs?.ext?.dsa; + } + const dsaRequest = dsaAuctions[bid.auctionId]; + let rejectReason; + if (dsaRequest) { + if (!bid.meta?.dsa) { + if (dsaRequest.dsarequired === 1) { + // request says dsa is supported; response does not have dsa info; warn about it + logWarn(`dsaControl: ${CONSTANTS.REJECTION_REASON.DSA_REQUIRED}; will still be accepted as regs.ext.dsa.dsarequired = 1`, bid); + } else if ([2, 3].includes(dsaRequest.dsarequired)) { + // request says dsa is required; response does not have dsa info; reject it + rejectReason = CONSTANTS.REJECTION_REASON.DSA_REQUIRED; + } + } else { + if (dsaRequest.pubrender === 0 && bid.meta.dsa.adrender === 0) { + // request says publisher can't render; response says advertiser won't; reject it + rejectReason = CONSTANTS.REJECTION_REASON.DSA_MISMATCH; + } else if (dsaRequest.pubrender === 2 && bid.meta.dsa.adrender === 1) { + // request says publisher will render; response says advertiser will; reject it + rejectReason = CONSTANTS.REJECTION_REASON.DSA_MISMATCH; + } + } + } + if (rejectReason) { + reject(rejectReason); + } else { + return fn.call(this, adUnitCode, bid, reject); + } +}); + +function toggleHooks(enabled) { + if (enabled && expiryHandle == null) { + getHook('addBidResponse').before(addBidResponseHook); + expiryHandle = auctionManager.onExpiry(auction => { + delete dsaAuctions[auction.getAuctionId()]; + }); + logInfo('dsaControl: DSA bid validation is enabled') + } else if (!enabled && expiryHandle != null) { + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + expiryHandle(); + expiryHandle = null; + logInfo('dsaControl: DSA bid validation is disabled') + } +} + +export function reset() { + toggleHooks(false); + dsaAuctions = {}; +} + +toggleHooks(true); + +config.getConfig('consentManagement', (cfg) => { + toggleHooks(cfg.consentManagement?.dsa?.validateBids ?? true); +}); diff --git a/modules/dsp_genieeBidAdapter.js b/modules/dsp_genieeBidAdapter.js new file mode 100644 index 00000000000..57aafd47fc8 --- /dev/null +++ b/modules/dsp_genieeBidAdapter.js @@ -0,0 +1,133 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { deepAccess, deepSetValue } from '../src/utils.js'; +import { config } from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const BIDDER_CODE = 'dsp_geniee'; +const ENDPOINT_URL = 'https://rt.gsspat.jp/prebid_auction'; +const ENDPOINT_URL_UNCOMFORTABLE = 'https://rt.gsspat.jp/prebid_uncomfortable'; +const ENDPOINT_USERSYNC = 'https://rt.gsspat.jp/prebid_cs'; +const VALID_CURRENCIES = ['USD', 'JPY']; +const converter = ortbConverter({ + context: { ttl: 300, netRevenue: true }, + // set optional parameters + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext', bidRequest.params); + return imp; + } +}); + +function USPConsent(consent) { + return typeof consent === 'string' && consent[0] === '1' && consent.toUpperCase()[2] === 'Y'; +} + +function invalidCurrency(currency) { + return typeof currency === 'string' && VALID_CURRENCIES.indexOf(currency.toUpperCase()) === -1; +} + +function hasTest(imp) { + if (typeof imp !== 'object') { + return false; + } + for (let i = 0; i < imp.length; i++) { + if (deepAccess(imp[i], 'ext.test') === 1) { + return true; + } + } + return false; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} _ The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (_) { + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} validBidRequests - an array of bids + * @param {BidderRequest} bidderRequest - the master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || // gdpr + USPConsent(bidderRequest.uspConsent) || // usp + config.getConfig('coppa') || // coppa + invalidCurrency(config.getConfig('currency.adServerCurrency')) // currency validation + ) { + return { + method: 'GET', + url: ENDPOINT_URL_UNCOMFORTABLE + }; + } + + const payload = converter.toORTB({ validBidRequests, bidderRequest }); + + if (hasTest(deepAccess(payload, 'imp'))) { + deepSetValue(payload, 'test', 1); + } + + deepSetValue(payload, 'at', 1); // first price auction only + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest - the master bidRequest object + * @return {bids} - An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse.body) { // empty response (no bids) + return []; + } + const bids = converter.fromORTB({ response: serverResponse.body, request: bidRequest.data }).bids; + return bids; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + // gdpr & usp + if (deepAccess(gdprConsent, 'gdprApplies') || USPConsent(uspConsent)) { + return syncs; + } + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: ENDPOINT_USERSYNC + }); + } + return syncs; + } +}; +registerBidder(spec); diff --git a/modules/dsp_genieeBidAdapter.md b/modules/dsp_genieeBidAdapter.md new file mode 100644 index 00000000000..d51d66884af --- /dev/null +++ b/modules/dsp_genieeBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +```markdown +Module Name: Geniee Bid Adapter +Module Type: Bidder Adapter +Maintainer: dsp_back@geniee.co.jp +``` + +# Description +This is [Geniee](https://geniee.co.jp) Bidder Adapter for Prebid.js. + +Please contact us before using the adapter. + +We will provide ads when satisfy the following conditions: + +- There are a certain number bid requests by zone +- The request is a Banner ad +- Payment is possible in Japanese yen or US dollars +- The request is not for GDPR or COPPA users + +Thus, even if the following test, it will be no bids if the request does not reach a certain requests. + +# Test AdUnits +```javascript +var adUnits={ + code: 'geniee-test-ad', + bids: [{ + bidder: 'dsp_geniee', + params: { + test: 1, + } + }], + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } +}; +``` diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index b8e812f581a..ea47c64094d 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -4,6 +4,10 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {includes} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'dspx'; const ENDPOINT_URL = 'https://buyer.dspx.tv/request/'; const ENDPOINT_URL_DEV = 'https://dcbuyer.dspx.tv/request/'; @@ -327,8 +331,8 @@ function getBannerSizes(bid) { /** * Parse size - * @param sizes - * @returns {width: number, h: height} + * @param size + * @returns {object} sizeObj */ function parseSize(size) { let sizeObj = {} diff --git a/modules/dxkultureBidAdapter.js b/modules/dxkultureBidAdapter.js new file mode 100644 index 00000000000..d803c476b6d --- /dev/null +++ b/modules/dxkultureBidAdapter.js @@ -0,0 +1,340 @@ +import { + logInfo, + logWarn, + logError, + logMessage, + deepAccess, + deepSetValue, + mergeDeep +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +const BIDDER_CODE = 'dxkulture'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_OUTSTREAM_RENDERER_URL = 'https://cdn.dxkulture.com/players/dxOutstreamPlayer.js'; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: '1.0.0', + } + }) + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(req, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return req; + }, + bidResponse(buildBidResponse, bid, context) { + let resMediaType; + const {bidRequest} = context; + + if (bid.adm?.trim().startsWith(' { + const userSync = deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + let syncDetails = []; + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + syncDetails = syncDetails.concat(value.syncs); + } + }); + syncDetails.forEach(syncDetails => { + let queryParamStrings = []; + let syncUrl = syncDetails.url; + + if (syncDetails.type === 'iframe') { + if (gdprConsent) { + queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + syncUrl = `${syncDetails.url}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}` + } + + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncUrl + }); + }); + + if (syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type == 'iframe'); + } else if (syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type == 'image'); + } + } + }); + logInfo('dxkulture.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; + +function outstreamRenderer(bid) { + const rendererConfig = { + width: bid.width, + height: bid.height, + vastTimeout: 5000, + maxAllowedVastTagRedirects: 3, + allowVpaid: false, + autoPlay: true, + preload: true, + mute: false + } + + const renderer = Renderer.install({ + id: bid.adId, + url: DEFAULT_OUTSTREAM_RENDERER_URL, + config: rendererConfig, + loaded: false, + targetId: bid.adUnitCode, + adUnitCode: bid.adUnitCode + }); + + try { + renderer.setRender(function (bid) { + bid.renderer.push(() => { + const { id, config } = bid.renderer; + window.dxOutstreamPlayer(bid, id, config); + }); + }); + } catch (err) { + logWarn('dxkulture: Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +/* ======================================= + * Util Functions + *======================================= */ + +function hasBannerMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +function hasVideoMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.video'); +} + +function _validateParams(bidRequest) { + if (!bidRequest.params) { + return false; + } + + if (bidRequest.params.e2etest) { + return true; + } + + if (!bidRequest.params.publisherId) { + logError('dxkulture: Validation failed: publisherId not declared'); + return false; + } + + if (!bidRequest.params.placementId) { + logError('dxkulture: Validation failed: placementId not declared'); + return false; + } + + const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); + if (!mediaTypesExists) { + return false; + } + + return true; +} + +/** + * Validates banner bid request. If it is not banner media type returns true. + * @param {BidRequest} bidRequest bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateBanner(bidRequest) { + // If there's no banner no need to validate + if (!hasBannerMediaType(bidRequest)) { + return true; + } + const banner = deepAccess(bidRequest, 'mediaTypes.banner'); + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; +} + +/** + * Validates video bid request. If it is not video media type returns true. + * @param {BidRequest} bidRequest, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateVideo(bidRequest) { + // If there's no video no need to validate + if (!hasVideoMediaType(bidRequest)) { + return true; + } + + const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + const params = deepAccess(bidRequest, 'params', {}); + + if (params && params.e2etest) { + return true; + } + + const videoParams = { + ...videoPlacement, + ...videoBidderParams // Bidder Specific overrides + }; + + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + logError('dxkulture: Validation failed: mimes are invalid'); + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + logError('dxkulture: Validation failed: protocols are invalid'); + return false; + } + + if (!videoParams.context) { + logError('dxkulture: Validation failed: context id not declared'); + return false; + } + + if (videoParams.context !== 'instream') { + logError('dxkulture: Validation failed: only context instream is supported '); + return false; + } + + if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { + logError('dxkulture: Validation failed: player size not declared or is not in format [[w,h]]'); + return false; + } + + return true; +} + +registerBidder(spec); diff --git a/modules/kulturemediaBidAdapter.md b/modules/dxkultureBidAdapter.md similarity index 86% rename from modules/kulturemediaBidAdapter.md rename to modules/dxkultureBidAdapter.md index 0bd17e97982..e31794ef6c6 100644 --- a/modules/kulturemediaBidAdapter.md +++ b/modules/dxkultureBidAdapter.md @@ -1,15 +1,15 @@ # Overview ``` -Module Name: Kulture Media Bid Adapter +Module Name: DXKulture Bid Adapter Module Type: Bidder Adapter Maintainer: devops@kulture.media ``` # Description -Module that connects to Kulture Media's demand sources. -Kulture Media bid adapter supports Banner and Video. +Module that connects to DXKulture's demand sources. +DXKulture bid adapter supports Banner and Video. # Test Parameters @@ -26,10 +26,12 @@ var adUnits = [ } }, bids: [{ - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { placementId: 'test', publisherId: 'test', + bidfloor: 2.7, + bidfloorcur: 'USD' } }] } @@ -42,7 +44,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid - 'mimes', - 'minduration', - 'maxduration', -- 'placement', +- 'plcmt', - 'protocols', - 'startdelay', - 'skip', @@ -73,13 +75,13 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid delivery: [2], minduration: 10, maxduration: 30, - placement: 1, + plcmt: 1, playbackmethod: [1,5], } }, bids: [ { - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { bidfloor: 0.5, publisherId: '12345', @@ -105,7 +107,7 @@ var adUnits = [ } }, bids: [{ - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { e2etest: true } @@ -129,7 +131,7 @@ var adUnits = [ }, bids: [ { - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { e2etest: true } diff --git a/modules/dynamicAdBoostRtdProvider.js b/modules/dynamicAdBoostRtdProvider.js new file mode 100644 index 00000000000..697bd7340d3 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.js @@ -0,0 +1,118 @@ +/** + * The {@link module:modules/realTimeData} module is required + * @module modules/dynamicAdBoost + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js' +import { loadExternalScript } from '../src/adloader.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { deepAccess, deepSetValue, isEmptyStr } from '../src/utils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const MODULE_NAME = 'dynamicAdBoost'; +const SCRIPT_URL = 'https://adxbid.info'; +const CLIENT_SUPPORTS_IO = window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype && + 'intersectionRatio' in window.IntersectionObserverEntry.prototype; +// Options for the Intersection Observer +const dabOptions = { + threshold: 0.5 // Trigger callback when 50% of the element is visible +}; +let observer; +let dabStartDate; +let dabStartTime; + +// Array of div IDs to track +let dynamicAdBoostAdUnits = {}; + +function init(config, userConsent) { + dabStartDate = new Date(); + dabStartTime = dabStartDate.getTime(); + if (!CLIENT_SUPPORTS_IO) { + return false; + } + // Create an Intersection Observer instance + observer = new IntersectionObserver(dabHandleIntersection, dabOptions); + if (config.params.keyId) { + let keyId = config.params.keyId; + if (keyId && !isEmptyStr(keyId)) { + let dabDivIdsToTrack = config.params.adUnits; + let dabInterval = setInterval(function() { + // Observe each div by its ID + dabDivIdsToTrack.forEach(divId => { + let div = document.getElementById(divId); + if (div) { + observer.observe(div); + } + }); + + let dabDateNow = new Date(); + let dabTimeNow = dabDateNow.getTime(); + let dabElapsedSeconds = Math.floor((dabTimeNow - dabStartTime) / 1000); + let elapsedThreshold = 30; + if (config.params.threshold) { + elapsedThreshold = config.params.threshold; + } + if (dabElapsedSeconds >= elapsedThreshold) { + clearInterval(dabInterval); // Stop + loadLmScript(keyId); + } + }, 1000); + + return true; + } + } + return false; +} + +function loadLmScript(keyId) { + let viewableAdUnits = Object.keys(dynamicAdBoostAdUnits); + let viewableAdUnitsCSV = viewableAdUnits.join(','); + const scriptUrl = `${SCRIPT_URL}/${keyId}.js?viewableAdUnits=${viewableAdUnitsCSV}`; + loadExternalScript(scriptUrl, MODULE_NAME); + observer.disconnect(); +} + +function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + const reqAdUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (Array.isArray(reqAdUnits)) { + reqAdUnits.forEach(adunit => { + let gptCode = deepAccess(adunit, 'code'); + if (dynamicAdBoostAdUnits.hasOwnProperty(gptCode)) { + // AdUnits has reached target viewablity at some point + deepSetValue(adunit, `ortb2Imp.ext.data.${MODULE_NAME}.${gptCode}`, dynamicAdBoostAdUnits[gptCode]); + } + }); + } + callback(); +} + +let markViewed = (entry, observer) => { + return () => { + observer.unobserve(entry.target); + } +} + +// Callback function when an observed element becomes visible +function dabHandleIntersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting && entry.intersectionRatio > 0.5) { + dynamicAdBoostAdUnits[entry.target.id] = entry.intersectionRatio; + markViewed(entry, observer) + } + }); +} + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init, + getBidRequestData, + markViewed +}; + +submodule('realTimeData', subModuleObj); diff --git a/modules/dynamicAdBoostRtdProvider.md b/modules/dynamicAdBoostRtdProvider.md new file mode 100644 index 00000000000..93efe3b3f97 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: Dynamic Ad Boost +Module Type: Track when a adunit is viewable +Maintainer: info@luponmedia.com + +# Description + +Enhance your revenue with the cutting-edge DynamicAdBoost module! By seamlessly integrating the powerful LuponMedia technology, our module retrieves adunits viewability data, providing publishers with valuable insights to optimize their revenue streams. To unlock the full potential of this technology, we provide a customized LuponMedia module tailored to your specific site requirements. Boost your ad revenue and gain unprecedented visibility into your performance with our advanced solution. + +In order to utilize this module, it is essential to collaborate with [LuponMedia](https://www.luponmedia.com/) to create an account and obtain detailed guidelines on configuring your sites. Working hand in hand with LuponMedia will ensure a smooth integration process, enabling you to fully leverage the capabilities of this module on your website. Take the first step towards optimizing your ad revenue and enhancing your site's performance by partnering with LuponMedia for a seamless experience. +Contact info@luponmedia.com for information. + +## Building Prebid with Real-time Data Support + +First, make sure to add the Dynamic AdBoost submodule to your Prebid.js package with: + +`gulp build --modules=rtdModule,dynamicAdBoostRtdProvider` + +The following configuration parameters are available: + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 2000, + dataProviders: [ + { + name: "dynamicAdBoost", + params: { + keyId: "[PROVIDED_KEY]", // Your provided Dynamic AdBoost keyId + adUnits: ["allowedAdUnit1", "allowedAdUnit2"], + threshold: 35 // optional + } + } + ] + } + ... +} +``` diff --git a/modules/e_volutionBidAdapter.js b/modules/e_volutionBidAdapter.js index 5f1b46ff9eb..2ce6cda16d1 100644 --- a/modules/e_volutionBidAdapter.js +++ b/modules/e_volutionBidAdapter.js @@ -4,6 +4,7 @@ import { isFn, deepAccess, logMessage } from '../src/utils.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'e_volution'; +const GVLID = 957; const AD_URL = 'https://service.e-volution.ai/?c=o&m=multi'; const URL_SYNC = 'https://service.e-volution.ai/?c=o&m=sync'; const NO_SYNC = true; @@ -57,6 +58,7 @@ function getUserId(eids, id, source, uidExt) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], noSync: NO_SYNC, diff --git a/modules/ebdrBidAdapter.js b/modules/ebdrBidAdapter.js index a03a1ec12ca..e830f8a94f7 100644 --- a/modules/ebdrBidAdapter.js +++ b/modules/ebdrBidAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, logInfo} from '../src/utils.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'ebdr'; diff --git a/modules/edge226BidAdapter.js b/modules/edge226BidAdapter.js new file mode 100644 index 00000000000..6d1e2466abe --- /dev/null +++ b/modules/edge226BidAdapter.js @@ -0,0 +1,188 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'edge226'; +const AD_URL = 'https://ssp.dauup.com/pbjs'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + 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)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + } +}; + +registerBidder(spec); diff --git a/modules/edge226BidAdapter.md b/modules/edge226BidAdapter.md new file mode 100644 index 00000000000..b38ff67065f --- /dev/null +++ b/modules/edge226BidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Edge226 Bidder Adapter +Module Type: Edge226 Bidder Adapter +Maintainer: audit@edge226.com +``` + +# Description + +Connects to Edge226 exchange for bids. +Edge226 bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 2216ab329b0..d57804c04e6 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,8 +1,9 @@ -import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined, isSlotMatchingAdUnitCode} from '../src/utils.js'; +import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'eplanning'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -267,6 +268,21 @@ function cleanName(name) { return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)|:/g, '_').replace(/^_+|_+$/g, ''); } +function getFloorStr(bid) { + if (typeof bid.getFloor === 'function') { + let bidFloor = bid.getFloor({ + currency: DOLLAR_CODE, + mediaType: '*', + size: '*' + }); + + if (bidFloor.floor) { + return '|' + encodeURIComponent(bidFloor.floor); + } + } + return ''; +} + function getSpaces(bidRequests, ml) { let impType = bidRequests.reduce((previousBits, bid) => (bid.mediaTypes && bid.mediaTypes[VIDEO]) ? (bid.mediaTypes[VIDEO].context == 'outstream' ? (previousBits | 2) : (previousBits | 1)) : previousBits, 0); // Only one type of auction is supported at a time @@ -286,7 +302,7 @@ function getSpaces(bidRequests, ml) { let sizeVast = firstSize ? firstSize.join('x') : DEFAULT_SIZE_VAST; name = 'video_' + sizeVast + '_' + i; es.map[name] = bid.bidId; - return name + ':' + sizeVast + ';1'; + return name + ':' + sizeVast + ';1' + getFloorStr(bid); } if (ml) { @@ -296,7 +312,7 @@ function getSpaces(bidRequests, ml) { } es.map[name] = bid.bidId; - return name + ':' + getSize(bid); + return name + ':' + getSize(bid) + getFloorStr(bid); }).join('+')).join('+'); return es; } diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 88d8f95b859..ce01abb9e71 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -1,7 +1,12 @@ -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; +import {getBidIdParameter} from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'eskimi'; // const ENDPOINT = 'https://hb.eskimi.com/bids' @@ -65,7 +70,7 @@ const CONVERTER = ortbConverter({ imp.secure = Number(window.location.protocol === 'https:'); if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; - imp.bidfloorcur = utils.getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } if (bidRequest.mediaTypes[VIDEO]) { diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index 6a3a0869c0e..d98dc02cdce 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -12,7 +12,7 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // RE below lint exception: UID2 and EUID are separate modules, but the protocol is the same and shared code makes sense here. // eslint-disable-next-line prebid/validate-imports -import { Uid2GetId, Uid2CodeVersion } from './uid2IdSystem_shared.js'; +import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; @@ -99,6 +99,15 @@ export const euidIdSubmodule = { internalStorage: ADVERTISING_COOKIE }; + if (FEATURES.UID2_CSTG) { + mappedConfig.cstg = { + serverPublicKey: config?.params?.serverPublicKey, + subscriptionId: config?.params?.subscriptionId, + optoutCheck: 1, + ...extractIdentityFromParams(config?.params ?? {}) + } + } + _logInfo(`EUID configuration loaded and mapped.`, mappedConfig); const result = Uid2GetId(mappedConfig, storage, _logInfo, _logWarn); _logInfo(`EUID getId returned`, result); return result; @@ -120,6 +129,10 @@ function decodeImpl(value) { const result = { euid: { id: value } }; return result; } + if (value.latestToken === 'optout') { + _logInfo('Found optout token. Refresh is unavailable for this token.'); + return { euid: { optout: true } }; + } if (Date.now() < value.latestToken.identity_expires) { return { euid: { id: value.latestToken.advertising_token } }; } diff --git a/modules/euidIdSystem.md b/modules/euidIdSystem.md index e3e16bce89d..9c3f730da83 100644 --- a/modules/euidIdSystem.md +++ b/modules/euidIdSystem.md @@ -1,9 +1,59 @@ ## EUID User ID Submodule -EUID requires initial tokens to be generated server-side. The EUID module handles storing, providing, and optionally refreshing them. The module can operate in one of two different modes: *Client Refresh* mode or *Server Only* mode. +The EUID module handles storing, providing, and optionally refreshing tokens. While initial tokens traditionally required server-side generation, the introduction of the *Client-Side Token Generation (CSTG)* mode offers publishers the flexibility to generate EUID tokens directly from the module, eliminating this need. Publishers can choose to operate the module in one of three distinct modes: *Client Refresh* mode, *Server Only* mode and *Client-Side Token Generation* mode. *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. +*Client-Side Token Generation* mode is included in EUID module by default. However, it's important to note that this mode was created and made available recently. For publishers who do not intend to use it, you have the option to instruct the build to exclude the code related to this feature: + +``` + $ gulp build --modules=euidIdSystem --disable UID2_CSTG +``` +If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the EUID Team first as they will provide required configuration values for you to use (see the Client-Side Token Generation (CSTG) mode section below for details) + +**This mode is created and made available recently. Please consult EUID Team first as they will provide required configuration values for you to use.** + +For publishers seeking a purely client-side integration without the complexities of server-side involvement, the CSTG mode is highly recommended. This mode requires the provision of a public key, subscription ID and [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) - either emails or phone numbers. In the CSTG mode, the module takes on the responsibility of encrypting the DII, generating the EUID token, and handling token refreshes when necessary. + +To configure the module to use this mode, you must: +1. Set `parmas.serverPublicKey` and `params.subscriptionId` (please reach out to the UID2 team to obtain these values) +2. Provide **ONLY ONE DII** by setting **ONLY ONE** of `params.email`/`params.phone`/`params.emailHash`/`params.phoneHash` + +Below is a table that provides guidance on when to use each directly identifying information (DII) parameter, along with information on whether normalization and hashing are required by the publisher for each parameter. + +| DII param | When to use it | Normalization required by publisher? | Hashing required by publisher? | +|------------------|-------------------------------------------------------|--------------------------------------|--------------------------------| +| params.email | When you have users' email address | No | No | +| params.phone | When you have user's phone number | Yes | No | +| params.emailHash | When you have user's hashed, normalized email address | Yes | Yes | +| params.phoneHash | When you have user's hashed, normalized phone number | Yes | Yes | + + +*Note that setting params.email will normalize email addresses, but params.phone requires phone numbers to be normalized.* + +Refer to [Normalization and Encoding](#normalization-and-encoding) for details on email address normalization, SHA-256 hashing and Base64 encoding. + +### CSTG example + +Configuration: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'euid', + params: { + serverPublicKey: '...server public key...', + subscriptionId: '...subcription id...', + email: 'user@email.com', + //phone: '+0000000', + //emailHash: '...email hash...', + //phoneHash: '...phone hash ...' + } + }] + } +}); +``` + ## Client Refresh mode This is the recommended mode for most scenarios. In this mode, the full response body from the EUID Token Generate or Token Refresh endpoint must be provided to the module. As long as the refresh token remains valid, the module will refresh the advertising token as needed. @@ -107,6 +157,11 @@ The module stores a number of internal values. By default, all values are stored `{`
  `"advertising_token": "...",`
  `"refresh_token": "...",`
  `"identity_expires": 1633643601000,`
  `"refresh_from": 1633643001000,`
  `"refresh_expires": 1636322000000,`
  `"refresh_response_key": "wR5t6HKMfJ2r4J7fEGX9Gw=="`
`}` +## Optout response + +`{`
  `"optout": "true",`
`}` + + ### Notes If you are trying to limit the size of cookies, provide the token in configuration and use the default option of local storage. diff --git a/modules/experianRtdProvider.js b/modules/experianRtdProvider.js new file mode 100644 index 00000000000..cd415d4b32c --- /dev/null +++ b/modules/experianRtdProvider.js @@ -0,0 +1,141 @@ +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { + deepAccess, + isArray, + isPlainObject, + isStr, + mergeDeep, + safeJSONParse, + timestamp +} from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/rtdModule/index.js').UserConsentData} UserConsentData + */ + +export const SUBMODULE_NAME = 'experian_rtid'; +export const EXPERIAN_RTID_DATA_KEY = 'experian_rtid_data'; +export const EXPERIAN_RTID_EXPIRATION_KEY = 'experian_rtid_expiration'; +export const EXPERIAN_RTID_STALE_KEY = 'experian_rtid_stale'; +export const EXPERIAN_RTID_NO_TRACK_KEY = 'experian_rtid_no_track'; +const EXPERIAN_RTID_URL = 'https://rtid.tapad.com' +const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); + +export const experianRtdObj = { + /** + * @summary modify bid request data + * @param {Object} reqBidsConfigObj + * @param {function} done + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + getBidRequestData(reqBidsConfigObj, done, config, userConsent) { + const dataEnvelope = storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + const stale = storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null); + const expired = storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null); + const noTrack = storage.getDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + const now = timestamp() + if (now > new Date(expired).getTime() || (noTrack == null && dataEnvelope == null)) { + // request data envelope and don't manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent) + done(); + return false; + } + if (now > new Date(stale).getTime()) { + // request data envelope and manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent); + } + if (noTrack != null) { + done(); + return false; + } + experianRtdObj.alterBids(reqBidsConfigObj, config); + done() + return true; + }, + + alterBids(reqBidsConfigObj, config) { + const dataEnvelope = safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null)); + if (dataEnvelope == null) { + return; + } + deepAccess(config, 'params.bidders').forEach((bidderCode) => { + const bidderData = dataEnvelope.find(({ bidder }) => bidder === bidderCode) + if (bidderData != null) { + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: { experianRtidKey: bidderData.data.key, experianRtidData: bidderData.data.data } }) + } + }) + }, + requestDataEnvelope(config, userConsent) { + function storeDataEnvelopeResponse(response) { + const responseJson = safeJSONParse(response); + if (responseJson != null) { + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, responseJson.staleAt, null); + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, responseJson.expiresAt, null); + if (responseJson.status === 'no_track') { + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + } else { + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify(responseJson.data), null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + } + } + } + const queryString = experianRtdObj.extractConsentQueryString(config, userConsent) + const fullUrl = queryString == null ? `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids` : `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids${queryString}` + ajax(fullUrl, storeDataEnvelopeResponse, null, { withCredentials: true, contentType: 'application/json' }) + }, + extractConsentQueryString(config, userConsent) { + const queryObj = {}; + + if (userConsent != null) { + if (userConsent.gdpr != null) { + const { gdprApplies, consentString } = userConsent.gdpr; + mergeDeep(queryObj, {gdpr: gdprApplies, gdpr_consent: consentString}) + } + if (userConsent.uspConsent != null) { + mergeDeep(queryObj, {us_privacy: userConsent.uspConsent}) + } + } + const consentQueryString = Object.entries(queryObj).map(([key, val]) => `${key}=${val}`).join('&'); + + let idsString = ''; + if (deepAccess(config, 'params.ids') != null && isPlainObject(deepAccess(config, 'params.ids'))) { + idsString = Object.entries(deepAccess(config, 'params.ids')).map(([idType, val]) => { + if (isArray(val)) { + return val.map((singleVal) => `id.${idType}=${singleVal}`).join('&') + } else { + return `id.${idType}=${val}` + } + }).join('&') + } + + const combinedString = [consentQueryString, idsString].filter((string) => string !== '').join('&'); + return combinedString !== '' ? `?${combinedString}` : undefined; + }, + /** + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @return {boolean} false to remove sub module + */ + init(config, userConsent) { + return isStr(deepAccess(config, 'params.accountId')); + } +} + +/** @type {RtdSubmodule} */ +export const experianRtdSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: experianRtdObj.getBidRequestData, + init: experianRtdObj.init +} + +submodule('realTimeData', experianRtdSubmodule); diff --git a/modules/experianRtdProvider.md b/modules/experianRtdProvider.md new file mode 100644 index 00000000000..ad46e0c3d55 --- /dev/null +++ b/modules/experianRtdProvider.md @@ -0,0 +1,52 @@ +# Experian Real-time Data Submodule + +## Overview + + Module Name: Experian Rtd Provider + Module Type: Rtd Provider + Maintainer: team-ui@tapad.com + +## Description + +The Experian RTD module adds encrypted identifier envelope to the bidding object. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,experianRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Experian RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Experian RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'experian_rtid', + waitForIt: true, + params: { + accountId: 'ZylatYg', + bidders: ['sovrn', 'pubmatic'], + ids: { maid: ['424', '2982'], hem: 'my-hem' } + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'experian_rtid' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.accountId | String | Your account id issued by Experian | | +| params.bidders | Array | List of bidders for which you would like data to be set | | +| params.ids | Record or string> | Additional identifiers to send to Experian RTID endpoint | | diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js index bc9c30cb479..f62bafcf637 100644 --- a/modules/fabrickIdSystem.js +++ b/modules/fabrickIdSystem.js @@ -10,6 +10,13 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getRefererInfo } from '../src/refererDetection.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + /** @type {Submodule} */ export const fabrickIdSubmodule = { /** diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 7b41f0fcc03..5ee9906b5df 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -3,6 +3,15 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').MediaType} MediaType + */ + /** * Version of the FeedAd bid adapter * @type {string} diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index f29ce7508d5..bda4494faaf 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -1,100 +1,109 @@ /** - * Fledge modules is responsible for registering fledged auction configs into the GPT slot; - * GPT is resposible to run the fledge auction. + * GPT-specific slot configuration logic for PAAPI. */ -import { config } from '../src/config.js'; -import { getHook } from '../src/hook.js'; -import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js'; -import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; +import {submodule} from '../src/hook.js'; +import {deepAccess, logInfo, logWarn} from '../src/utils.js'; +import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; -const MODULE = 'fledgeForGpt' +// import parent module to keep backwards-compat for NPM consumers after paapi was split from fledgeForGpt +// there's a special case in webpack.conf.js to avoid duplicating build output on non-npm builds +// TODO: remove this in prebid 9 +// eslint-disable-next-line prebid/validate-imports +import './paapi.js'; +const MODULE = 'fledgeForGpt'; -export let isEnabled = false; +let getPAAPIConfig; -config.getConfig('fledgeForGpt', config => init(config.fledgeForGpt)); +// for backwards compat, we attempt to automatically set GPT configuration as soon as we +// have the auction configs available. Disabling this allows one to call pbjs.setPAAPIConfigForGPT at their +// own pace. +let autoconfig = true; -/** - * Module init. - */ -export function init(cfg) { - if (cfg && cfg.enabled === true) { - if (!isEnabled) { - getHook('addComponentAuction').before(addComponentAuctionHook); - isEnabled = true; - } - logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg); - } else { - if (isEnabled) { - getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove(); - isEnabled = false; - } - logInfo(`${MODULE} disabled`, cfg); - } -} +Object.entries({ + [MODULE]: MODULE, + 'paapi': 'paapi.gpt' +}).forEach(([topic, ns]) => { + const configKey = `${ns}.autoconfig`; + config.getConfig(topic, (cfg) => { + autoconfig = deepAccess(cfg, configKey, true); + }); +}); -export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) { - const seller = componentAuctionConfig.seller; - const gptSlot = getGptSlotForAdUnitCode(adUnitCode); - if (gptSlot && gptSlot.setConfig) { - gptSlot.setConfig({ - componentAuction: [{ - configKey: seller, - auctionConfig: componentAuctionConfig - }] - }); - logInfo(MODULE, `register component auction config for: ${adUnitCode} x ${seller}: ${gptSlot.getAdUnitPath()}`, componentAuctionConfig); - } else { - logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`); - } - - next(adUnitCode, componentAuctionConfig); +export function slotConfigurator() { + const PREVIOUSLY_SET = {}; + return function setComponentAuction(adUnitCode, auctionConfigs, reset = true) { + const gptSlot = getGptSlotForAdUnitCode(adUnitCode); + if (gptSlot && gptSlot.setConfig) { + let previous = PREVIOUSLY_SET[adUnitCode] ?? {}; + let configsBySeller = Object.fromEntries(auctionConfigs.map(cfg => [cfg.seller, cfg])); + const sellers = Object.keys(configsBySeller); + if (reset) { + configsBySeller = Object.assign(previous, configsBySeller); + previous = Object.fromEntries(sellers.map(seller => [seller, null])); + } else { + sellers.forEach(seller => { + previous[seller] = null; + }); + } + Object.keys(previous).length ? PREVIOUSLY_SET[adUnitCode] = previous : delete PREVIOUSLY_SET[adUnitCode]; + const componentAuction = Object.entries(configsBySeller) + .map(([configKey, auctionConfig]) => ({configKey, auctionConfig})); + if (componentAuction.length > 0) { + gptSlot.setConfig({componentAuction}); + logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); + } + } else if (auctionConfigs.length > 0) { + logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); + } + }; } -function isFledgeSupported() { - return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator -} +const setComponentAuction = slotConfigurator(); -export function markForFledge(next, bidderRequests) { - if (isFledgeSupported()) { - bidderRequests.forEach((req) => { - req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) - }) - } - next(bidderRequests); -} -getHook('makeBidRequests').after(markForFledge); - -export function setImpExtAe(imp, bidRequest, context) { - if (!context.bidderRequest.fledgeEnabled) { - delete imp.ext?.ae; +export function onAuctionConfigFactory(setGptConfig = setComponentAuction) { + return function onAuctionConfig(auctionId, configsByAdUnit, markAsUsed) { + if (autoconfig) { + Object.entries(configsByAdUnit).forEach(([adUnitCode, cfg]) => { + setGptConfig(adUnitCode, cfg?.componentAuctions ?? []); + markAsUsed(adUnitCode); + }); + } } } -registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); -// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up -// fledge response processing in two steps: first aggregate all the auction configs by their imp... - -export function parseExtPrebidFledge(response, ortbResponse, context) { - (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => { - const impCtx = context.impContext[cfg.impid]; - if (!impCtx?.imp?.ext?.ae) { - logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp); - } else { - impCtx.fledgeConfigs = impCtx.fledgeConfigs || []; - impCtx.fledgeConfigs.push(cfg); +export function setPAAPIConfigFactory( + getConfig = (filters) => getPAAPIConfig(filters, true), + setGptConfig = setComponentAuction) { + /** + * Configure GPT slots with PAAPI auction configs. + * `filters` are the same filters accepted by `pbjs.getPAAPIConfig`; + */ + return function(filters = {}) { + let some = false; + Object.entries( + getConfig(filters) || {} + ).forEach(([au, config]) => { + if (config != null) { + some = true; + } + setGptConfig(au, config?.componentAuctions || [], true); + }) + if (!some) { + logInfo(`${MODULE}: No component auctions available to set`); } - }) + } } -registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]}); - -// ...then, make them available in the adapter's response. This is the client side version, for which the -// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]} +/** + * Configure GPT slots with PAAPI component auctions. Accepts the same filter arguments as `pbjs.getPAAPIConfig`. + */ +getGlobal().setPAAPIConfigForGPT = setPAAPIConfigFactory(); -export function setResponseFledgeConfigs(response, ortbResponse, context) { - const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({bidId: impCtx.bidRequest.bidId, config: cfg.config}))); - if (configs.length > 0) { - response.fledgeAuctionConfigs = configs; +submodule('paapi', { + name: 'gpt', + onAuctionConfig: onAuctionConfigFactory(), + init(params) { + getPAAPIConfig = params.getPAAPIConfig; } -} -registerOrtbProcessor({type: RESPONSE, name: 'fledgeAuctionConfigs', priority: -1, fn: setResponseFledgeConfigs, dialects: [PBS]}) +}); diff --git a/modules/fledgeForGpt.md b/modules/fledgeForGpt.md index 3bb86cd5946..28f44da6459 100644 --- a/modules/fledgeForGpt.md +++ b/modules/fledgeForGpt.md @@ -15,8 +15,8 @@ This is accomplished by adding the `fledgeForGpt` module to the list of modules gulp build --modules=fledgeForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. To provide a high degree of flexiblity for testing, FLEDGE -settings exist at the module level, the bidder level, and the slot level. +Second, they must enable FLEDGE in their Prebid.js configuration. +This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. ### Module Configuration This module exposes the following settings: @@ -24,15 +24,20 @@ This module exposes the following settings: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | |enabled | Boolean |Enable/disable the module |Defaults to `false` | +|bidders | Array[String] |Optional list of bidders |Defaults to all bidders | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module -using the `setConfig` method of Prebid.js: +As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). +using the `setConfig` method of Prebid.js. Optionally, a list of +bidders to apply these settings to may be provided: ```js pbjs.que.push(function() { pbjs.setConfig({ fledgeForGpt: { - enabled: true + enabled: true, + bidders: ['openx', 'rtbhouse'], + defaultForSlots: 1 } }); }); @@ -44,23 +49,25 @@ This module adds the following setting for bidders: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | | fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| -In addition to enabling FLEDGE at the module level, individual bidders must also be enabled. This allows publishers to -selectively test with one or more bidders as they desire. To enable one or more bidders, use the `setBidderConfig` method +Individual bidders may be further included or excluded here using the `setBidderConfig` method of Prebid.js: ```js pbjs.setBidderConfig({ bidders: ["openx"], config: { - fledgeEnabled: true + fledgeEnabled: true, + defaultForSlots: 1 } }); ``` ### AdUnit Configuration -Enabling an adunit for FLEDGE eligibility is accomplished by setting an attribute of the `ortb2Imp` object for that -adunit. +All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. +If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that +adunit. This attribute will take precedence over `defaultForSlots` setting. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | diff --git a/modules/flippBidAdapter.js b/modules/flippBidAdapter.js new file mode 100644 index 00000000000..f9c424d9da5 --- /dev/null +++ b/modules/flippBidAdapter.js @@ -0,0 +1,196 @@ +import {isEmpty, parseUrl} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const NETWORK_ID = 10922; +const AD_TYPES = [4309, 641]; +const DTX_TYPES = [5061]; +const TARGET_NAME = 'inline'; +const BIDDER_CODE = 'flipp'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +const DEFAULT_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_CREATIVE_TYPE = 'NativeX'; +const VALID_CREATIVE_TYPES = ['DTX', 'NativeX']; +const FLIPP_USER_KEY = 'flipp-uid'; +const COMPACT_DEFAULT_HEIGHT = 600; + +let userKey = null; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function getUserKey(options = {}) { + if (userKey) { + return userKey; + } + + // If the partner provides the user key use it, otherwise fallback to cookies + if ('userKey' in options && options.userKey) { + if (isValidUserKey(options.userKey)) { + userKey = options.userKey; + return options.userKey; + } + } + + // Grab from Cookie + const foundUserKey = storage.cookiesAreEnabled(null) && storage.getCookie(FLIPP_USER_KEY, null); + if (foundUserKey && isValidUserKey(foundUserKey)) { + return foundUserKey; + } + + // Generate if none found + userKey = generateUUID(); + + // Set cookie + if (storage.cookiesAreEnabled()) { + storage.setCookie(FLIPP_USER_KEY, userKey); + } + + return userKey; +} + +function isValidUserKey(userKey) { + return typeof userKey === 'string' && !userKey.startsWith('#') && userKey.length > 0; +} + +const generateUUID = () => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}; + +/** + * Determines if a creativeType is valid + * + * @param {string} creativeType The Creative Type to validate. + * @return string creativeType if this is a valid Creative Type, and 'NativeX' otherwise. + */ +const validateCreativeType = (creativeType) => { + if (creativeType && VALID_CREATIVE_TYPES.includes(creativeType)) { + return creativeType; + } else { + return DEFAULT_CREATIVE_TYPE; + } +}; + +const getAdTypes = (creativeType) => { + if (creativeType === 'DTX') { + return DTX_TYPES; + } + return AD_TYPES; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid.params.siteId) && !!(bid.params.publisherNameIdentifier); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const urlParams = parseUrl(bidderRequest.refererInfo.page).search; + const contentCode = urlParams['flipp-content-code']; + const userKey = getUserKey(validBidRequests[0]?.params); + const placements = validBidRequests.map((bid, index) => { + const options = bid.params.options || {}; + if (!options.hasOwnProperty('startCompact')) { + options.startCompact = true; + } + return { + divName: TARGET_NAME, + networkId: NETWORK_ID, + siteId: bid.params.siteId, + adTypes: getAdTypes(bid.params.creativeType), + count: 1, + ...(!isEmpty(bid.params.zoneIds) && {zoneIds: bid.params.zoneIds}), + properties: { + ...(!isEmpty(contentCode) && {contentCode: contentCode.slice(0, 32)}), + }, + options, + prebid: { + requestId: bid.bidId, + publisherNameIdentifier: bid.params.publisherNameIdentifier, + height: bid.mediaTypes.banner.sizes[index][0], + width: bid.mediaTypes.banner.sizes[index][1], + creativeType: validateCreativeType(bid.params.creativeType), + } + } + }); + return { + method: 'POST', + url: ENDPOINT, + data: { + placements, + url: bidderRequest.refererInfo.page, + user: { + key: userKey, + }, + }, + } + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest A bid request object + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse?.body) return []; + const placements = bidRequest.data.placements; + const res = serverResponse.body; + if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) { + return res.decisions.inline.map(decision => { + const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId); + const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height; + return { + bidderCode: BIDDER_CODE, + requestId: decision.prebid?.requestId, + cpm: decision.prebid?.cpm, + width: decision.width, + height, + creativeId: decision.adId, + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: DEFAULT_TTL, + ad: decision.prebid?.creative, + } + }); + } + return []; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: (syncOptions, serverResponses) => [], +} +registerBidder(spec); diff --git a/modules/flippBidAdapter.md b/modules/flippBidAdapter.md new file mode 100644 index 00000000000..e823432a60f --- /dev/null +++ b/modules/flippBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Flipp Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@flipp.com +``` + +# Description + +This module connects publishers to Flipp's Shopper Experience via Prebid.js. + + +# Test parameters + +```javascript +var adUnits = [ + { + code: 'flipp-scroll-ad-content', + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + bids: [ + { + bidder: 'flipp', + params: { + creativeType: 'NativeX', // Optional, can be one of 'NativeX' (default) or 'DTX' + publisherNameIdentifier: 'wishabi-test-publisher', // Required + siteId: 1192075, // Required + zoneIds: [260678], // Optional + userKey: ``, // Optional, but recommended for better user experience. Can be a cookie, session id or any other user identifier + options: { + startCompact: true, // Optional. Height of the experience will be reduced. Default to true + dwellExpand: true // Optional. Auto expand the experience after a certain time passes. Default to true + } + } + } + ] + } +] +``` diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index edb750a6b90..c0ae55efc89 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -2,22 +2,18 @@ import { _each, deepSetValue, isEmpty } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'fluct'; const END_POINT = 'https://hb.adingo.jp/prebid'; const VERSION = '1.2'; const NET_REVENUE = true; const TTL = 300; -/** - * See modules/userId/eids.js for supported sources - */ -const SUPPORTED_USER_ID_SOURCES = [ - 'adserver.org', - 'criteo.com', - 'intimatemerger.com', - 'liveramp.com', -]; - export const spec = { code: BIDDER_CODE, aliases: ['adingo'], @@ -35,7 +31,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids. + * @param {validBidRequests} validBidRequests an array of bids. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { @@ -43,16 +40,24 @@ export const spec = { const page = bidderRequest.refererInfo.page; _each(validBidRequests, (request) => { + const impExt = request.ortb2Imp?.ext; const data = Object(); data.page = page; data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; - data.transactionId = request.ortb2Imp?.ext?.tid; data.user = { - eids: (request.userIdAsEids || []).filter((eid) => SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1) + data: bidderRequest.ortb2?.user?.data ?? [], + eids: [ + ...(request.userIdAsEids ?? []), + ...(bidderRequest.ortb2?.user?.ext?.eids ?? []), + ], }; + if (impExt) { + data.transactionId = impExt.tid; + data.gpid = impExt.gpid ?? impExt.data?.pbadslot ?? impExt.data?.adserver?.adslot; + } if (bidderRequest.gdprConsent) { deepSetValue(data, 'regs.gdpr', { consent: bidderRequest.gdprConsent.consentString, diff --git a/modules/freepassBidAdapter.js b/modules/freepassBidAdapter.js index 02e433fa8fc..cdcc3c6a4b0 100644 --- a/modules/freepassBidAdapter.js +++ b/modules/freepassBidAdapter.js @@ -48,7 +48,7 @@ export const spec = { isBidRequestValid(bid) { logMessage('Validating bid: ', bid); - return !!bid.adUnitCode; + return !(!bid.adUnitCode || !bid.params || !bid.params.publisherId); }, buildRequests(validBidRequests, bidderRequest) { @@ -72,6 +72,25 @@ export const spec = { data.user = prepareUserInfo(data.user, freepassId); data.device = prepareDeviceInfo(data.device, freepassId); + // set site.page & site.publisher + data.site = data.site || {}; + data.site.publisher = data.site.publisher || {}; + // set site.publisher.id. from params.publisherId required + data.site.publisher.id = validBidRequests[0].params.publisherId; + // set site.publisher.domain from params.publisherUrl. optional + data.site.publisher.domain = validBidRequests[0].params?.publisherUrl; + + // set source + data.source = data.source || {}; + data.source.fd = 0; + data.source.tid = validBidRequests.ortb2?.source?.tid; + data.source.pchain = ''; + + // set imp.ext + validBidRequests.forEach((bidRequest, index) => { + data.imp[index].tagId = bidRequest.adUnitCode; + }); + data.test = validBidRequests[0].test || 0; logMessage('FreePass BidAdapter augmented ORTB bid request user: ', data.user); diff --git a/modules/freepassBidAdapter.md b/modules/freepassBidAdapter.md index 60957a1fbe5..7b56a469583 100644 --- a/modules/freepassBidAdapter.md +++ b/modules/freepassBidAdapter.md @@ -23,7 +23,10 @@ This BidAdapter requires the FreePass IdSystem to be configured. Please contact } }, bids: [{ - bidder: 'freepass' + bidder: 'freepass', + params: { + publisherId: '12345' + } }] } ]; diff --git a/modules/freepassIdSystem.js b/modules/freepassIdSystem.js index d52c537e800..419aa9ec414 100644 --- a/modules/freepassIdSystem.js +++ b/modules/freepassIdSystem.js @@ -1,8 +1,12 @@ import { submodule } from '../src/hook.js'; -import {generateUUID, logMessage} from '../src/utils.js'; +import { logMessage } from '../src/utils.js'; +import { getCoreStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'freepassId'; +export const FREEPASS_COOKIE_KEY = '_f_UF8cCRlr'; +export const storage = getCoreStorageManager(MODULE_NAME); + export const freepassIdSubmodule = { name: MODULE_NAME, decode: function (value, config) { @@ -15,7 +19,12 @@ export const freepassIdSubmodule = { logMessage('Getting FreePass ID using config: ' + JSON.stringify(config)); const freepassData = config.params !== undefined ? (config.params.freepassData || {}) : {} - let idObject = {userId: generateUUID()}; + const idObject = {}; + + const userId = storage.getCookie(FREEPASS_COOKIE_KEY); + if (userId !== null) { + idObject.userId = userId; + } if (freepassData.commonId !== undefined) { idObject.commonId = config.params.freepassData.commonId; @@ -29,8 +38,8 @@ export const freepassIdSubmodule = { }, extendId: function (config, consent, cachedIdObject) { - let freepassData = config.params.freepassData; - let hasFreepassData = freepassData !== undefined; + const freepassData = config.params.freepassData; + const hasFreepassData = freepassData !== undefined; if (!hasFreepassData) { logMessage('No Freepass Data. CachedIdObject will not be extended: ' + JSON.stringify(cachedIdObject)); return { @@ -38,12 +47,7 @@ export const freepassIdSubmodule = { }; } - if (freepassData.commonId === cachedIdObject.commonId && freepassData.userIp === cachedIdObject.userIp) { - logMessage('FreePass ID is already up-to-date: ' + JSON.stringify(cachedIdObject)); - return { - id: cachedIdObject - }; - } + const currentCookieId = storage.getCookie(FREEPASS_COOKIE_KEY); logMessage('Extending FreePass ID object: ' + JSON.stringify(cachedIdObject)); logMessage('Extending FreePass ID using config: ' + JSON.stringify(config)); @@ -52,8 +56,8 @@ export const freepassIdSubmodule = { id: { commonId: freepassData.commonId, userIp: freepassData.userIp, - userId: cachedIdObject.userId, - }, + userId: currentCookieId + } }; } }; diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index cd4785cdc78..e11aa3f8fb7 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -3,7 +3,13 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'freewheel-ssp'; +const GVL_ID = 285; const PROTOCOL = getProtocol(); const FREEWHEEL_ADSSETUP = PROTOCOL + '://ads.stickyadstv.com/www/delivery/swfIndex.php'; @@ -182,8 +188,8 @@ function getCampaignId(xmlNode) { } /** -* returns the top most accessible window -*/ + * returns the top most accessible window + */ function getTopMostWindow() { var res = window; @@ -314,24 +320,25 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: ['stickyadstv', 'freewheelssp'], // aliases for freewheel-ssp /** - * 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. - */ + * 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.zoneId); }, /** - * 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. - */ + * 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) { // var currency = config.getConfig(currency); @@ -382,6 +389,15 @@ export const spec = { requestParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; } + // Add content object + if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.site && bidderRequest.ortb2.site.content && typeof bidderRequest.ortb2.site.content === 'object') { + try { + requestParams._fw_prebid_content = JSON.stringify(bidderRequest.ortb2.site.content); + } catch (error) { + logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the content object: ' + error); + } + } + // Add schain object var schain = currentBidRequest.schain; if (schain) { @@ -463,12 +479,12 @@ export const spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @param {object} request: the built request object containing the initial bidRequest. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {object} request the built request object containing the initial bidRequest. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, request) { var bidrequest = request.bidRequest; var playerSize = []; @@ -532,10 +548,11 @@ export const spec = { }; if (bidrequest.mediaTypes.video) { - bidResponse.vastXml = serverResponse; bidResponse.mediaType = 'video'; } + bidResponse.vastXml = serverResponse; + bidResponse.ad = formatAdHTML(bidrequest, playerSize); bidResponses.push(bidResponse); } diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 809f1311c42..1794c3f76f4 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -12,6 +12,13 @@ import {uspDataHandler} from '../src/adapterManager.js'; import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'ftrackId'; const LOG_PREFIX = 'FTRACK - '; const LOCAL_STORAGE_EXP_DAYS = 30; diff --git a/modules/gammaBidAdapter.js b/modules/gammaBidAdapter.js index 279eb78812e..dadfe2ab14b 100644 --- a/modules/gammaBidAdapter.js +++ b/modules/gammaBidAdapter.js @@ -1,8 +1,17 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -const ENDPOINT = 'https://hb.gammaplatform.com'; -const ENDPOINT_USERSYNC = 'https://cm-supply-web.gammaplatform.com'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'gamma'; +const ENDPOINTS = { + SGP: 'https://hb.gammaplatform.com', + JPN: 'https://hb-jp.gammaplatform.com', + US_WEST: 'https://hb-us.gammaplatform.com', + EU: 'https://hb-eu.gammaplatform.com' +} export const spec = { code: BIDDER_CODE, @@ -28,8 +37,10 @@ export const spec = { buildRequests: function(bidRequests, bidderRequest) { const serverRequests = []; const bidderRequestReferer = bidderRequest?.refererInfo?.page || ''; + let ENDPOINT; for (var i = 0, len = bidRequests.length; i < len; i++) { const gaxObjParams = bidRequests[i]; + ENDPOINT = getAdUrlByRegion(gaxObjParams); serverRequests.push({ method: 'GET', url: ENDPOINT + '/adx/request?wid=' + gaxObjParams.params.siteId + '&zid=' + gaxObjParams.params.zoneId + '&hb=pbjs&bidid=' + gaxObjParams.bidId + '&urf=' + encodeURIComponent(bidderRequestReferer) @@ -55,16 +66,45 @@ export const spec = { } return bids; - }, + } +} + +/** + * Get endpoint url by region + * @param bid + * @return aUrl + */ +function getAdUrlByRegion(bid) { + let ENDPOINT; + + if (bid.params.region && ENDPOINTS[bid.params.region]) { + ENDPOINT = ENDPOINTS[bid.params.region]; + } else { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; - getUserSyncs: function(syncOptions) { - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: ENDPOINT_USERSYNC + '/adx/usersync' - }]; + switch (region) { + case 'Europe': + ENDPOINT = ENDPOINTS['EU']; + break; + case 'Australia': + ENDPOINT = ENDPOINTS['JPN']; + break; + case 'Asia': + ENDPOINT = ENDPOINTS['SGP']; + break; + case 'America': + ENDPOINT = ENDPOINTS['US_WEST']; + break; + default: ENDPOINT = ENDPOINTS['SGP']; + } + } catch (err) { + ENDPOINT = ENDPOINTS['SGP']; } } + + return ENDPOINT; } /** diff --git a/modules/gammaBidAdapter.md b/modules/gammaBidAdapter.md index 2902be78492..bcb26d0b86e 100644 --- a/modules/gammaBidAdapter.md +++ b/modules/gammaBidAdapter.md @@ -12,6 +12,14 @@ Connects to Gamma exchange for bids. Gamma bid adapter supports Banner, Video. +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :------------------------ | :------------------- | +| `zoneId` | required | Zone ID | "1398219417" | +| `siteId` | required | Website ID | "1398219351" | +| `region` | optional (for prebid.js) | Prefix of the region to which prebid must send requests. Possible values: "SGP", "JPN", "US_WEST", "EU" | "SGP" | + # Test Parameters: For Banner ``` var adUnits = [{ @@ -22,8 +30,9 @@ var adUnits = [{ bids: [{ bidder: 'gamma', params: { - siteId: '1465446377', - zoneId: '1515999290' + siteId: '1398219351', + zoneId: '1398219417', + region: 'SGP' } }] @@ -39,8 +48,9 @@ var adUnits = [{ bids: [{ bidder: 'gamma', params: { - siteId: '1465446377', - zoneId: '1493280341' + siteId: '1398219351', + zoneId: '1614755846', + region: 'SGP' } }] @@ -59,8 +69,9 @@ In order to receive bids please map localhost to (any) test domain. bids: [{ bidder: 'gamma', params: { - siteId: '1465446377', - zoneId: '1515999290' + siteId: '1398219351', + zoneId: '1398219417', + region: 'SGP' } }] }]; diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 4140c1a54f4..5b73ec19e08 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -5,10 +5,9 @@ import {deepAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; -import {find} from '../src/polyfill.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID, FIRST_PARTY_GVLID} from '../src/consentHandler.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, @@ -27,44 +26,62 @@ import { ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS, - ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_UFPD + ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD } from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; -const TCF2 = { - purpose1: {id: 1, name: 'storage'}, - purpose2: {id: 2, name: 'basicAds'}, - purpose4: {id: 4, name: 'personalizedAds'}, - purpose7: {id: 7, name: 'measurement'}, +export const ACTIVE_RULES = { + purpose: {}, + feature: {} }; -/* - These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher. -*/ -const DEFAULT_RULES = [{ - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}, { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}]; - -export let purpose1Rule; -export let purpose2Rule; -export let purpose4Rule; -export let purpose7Rule; - -export let enforcementRules; +const CONSENT_PATHS = { + purpose: 'purpose.consents', + feature: 'specialFeatureOptins' +}; + +const CONFIGURABLE_RULES = { + storage: { + type: 'purpose', + default: { + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }, + id: 1, + }, + basicAds: { + type: 'purpose', + id: 2, + default: { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + } + }, + personalizedAds: { + type: 'purpose', + id: 4, + }, + measurement: { + type: 'purpose', + id: 7, + }, + transmitPreciseGeo: { + type: 'feature', + id: 1, + }, +}; const storageBlocked = new Set(); const biddersBlocked = new Set(); const analyticsBlocked = new Set(); const ufpdBlocked = new Set(); +const eidsBlocked = new Set(); +const geoBlocked = new Set(); let hooksAdded = false; let strictStorageEnforcement = false; @@ -79,6 +96,9 @@ const GVLID_LOOKUP_PRIORITY = [ const RULE_NAME = 'TCF2'; const RULE_HANDLES = []; +// in JS we do not have access to the GVL; assume that everyone declares legitimate interest for basic ads +const LI_PURPOSES = [2]; + /** * Retrieve a module's GVL ID. */ @@ -91,7 +111,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { if (gvlMapping && gvlMapping[moduleName]) { return gvlMapping[moduleName]; } else if (moduleType === MODULE_TYPE_PREBID) { - return VENDORLESS_GVLID; + return moduleName === 'cdep' ? FIRST_PARTY_GVLID : VENDORLESS_GVLID; } else { let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); if (gvlid == null && Object.keys(modules).length > 0) { @@ -143,6 +163,17 @@ export function shouldEnforce(consentData, purpose, name) { return consentData && consentData.gdprApplies; } +function getConsent(consentData, type, id, gvlId) { + let purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${id}`); + let vendor = !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); + + if (type === 'purpose' && LI_PURPOSES.includes(id)) { + purpose ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${id}`); + vendor ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`); + } + return {purpose, vendor}; +} + /** * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, * the caller may decide to suppress a TCF-sensitive activity. @@ -153,55 +184,32 @@ export function shouldEnforce(consentData, purpose, name) { * @returns {boolean} */ export function validateRules(rule, consentData, currentModule, gvlId) { - const purposeId = TCF2[Object.keys(TCF2).filter(purposeName => TCF2[purposeName].name === rule.purpose)[0]].id; + const ruleOptions = CONFIGURABLE_RULES[rule.purpose]; // return 'true' if vendor present in 'vendorExceptions' if ((rule.vendorExceptions || []).includes(currentModule)) { return true; } - const vendorConsentRequred = !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); - - // get data from the consent string - const purposeConsent = deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); - const vendorConsent = vendorConsentRequred ? deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`) : true; - const liTransparency = deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`); - - /* - Since vendor exceptions have already been handled, the purpose as a whole is allowed if it's not being enforced - or the user has consented. Similar with vendors. - */ - const purposeAllowed = rule.enforcePurpose === false || purposeConsent === true; - const vendorAllowed = rule.enforceVendor === false || vendorConsent === true; - - /* - Few if any vendors should be declaring Legitimate Interest for Device Access (Purpose 1), but some are claiming - LI for Basic Ads (Purpose 2). Prebid.js can't check to see who's declaring what legal basis, so if LI has been - established for Purpose 2, allow the auction to take place and let the server sort out the legal basis calculation. - */ - if (purposeId === 2) { - return (purposeAllowed && vendorAllowed) || (liTransparency === true); + const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); + const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId); + + let validation = (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); + + if (gvlId === FIRST_PARTY_GVLID) { + validation = (!rule.enforcePurpose || !!deepAccess(consentData, `vendorData.publisher.consents.${ruleOptions.id}`)); } - return purposeAllowed && vendorAllowed; + return validation; } -/** - * all activity rules follow the same structure: - * if GDPR is in scope, check configuration for a particular purpose, and if that enables enforcement, - * check against consent data for that purpose and vendor - * - * @param purposeNo TCF purpose number to check for this activity - * @param getEnforcementRule getter for gdprEnforcement rule definition to use - * @param blocked optional set to use for collecting denied vendors - * @param gvlidFallback optional factory function for a gvlid falllback function - */ -function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = () => null) { +function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback = () => null) { return function (params) { const consentData = gdprDataHandler.getConsentData(); const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; + if (shouldEnforce(consentData, purposeNo, modName)) { const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params)); - let allow = !!validateRules(getEnforcementRule(), consentData, modName, gvlid); + let allow = !!checkConsent(consentData, modName, gvlid); if (!allow) { blocked && blocked.add(modName); return {allow}; @@ -210,32 +218,62 @@ function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = }; } -export const accessDeviceRule = ((rule) => { - return function (params) { - // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; - return rule(params); - }; -})(gdprRule(1, () => purpose1Rule, storageBlocked)); - -export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); -export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked); +function singlePurposeGdprRule(purposeNo, blocked = null, gvlidFallback = () => null) { + return gdprRule(purposeNo, (cd, modName, gvlid) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid), blocked, gvlidFallback); +} -export const fetchBidsRule = ((rule) => { +function exceptPrebidModules(ruleFn) { return function (params) { - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID) { // TODO: this special case is for the PBS adapter (componentType is 'prebid') // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; // that is, however, a breaking change and skipped for now return; } + return ruleFn(params); + }; +} + +export const accessDeviceRule = ((rule) => { + return function (params) { + // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; return rule(params); }; -})(gdprRule(2, () => purpose2Rule, biddersBlocked)); +})(singlePurposeGdprRule(1, storageBlocked)); + +export const syncUserRule = singlePurposeGdprRule(1, storageBlocked); +export const enrichEidsRule = singlePurposeGdprRule(1, storageBlocked); +export const fetchBidsRule = exceptPrebidModules(singlePurposeGdprRule(2, biddersBlocked)); +export const reportAnalyticsRule = singlePurposeGdprRule(7, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); +export const ufpdRule = singlePurposeGdprRule(4, ufpdBlocked); + +export const transmitEidsRule = exceptPrebidModules((() => { + // Transmit EID special case: + // by default, legal basis or vendor exceptions for any purpose between 2 and 10 + // (but disregarding enforcePurpose and enforceVendor config) is enough to allow EIDs through + function check2to10Consent(consentData, modName, gvlId) { + for (let pno = 2; pno <= 10; pno++) { + if (ACTIVE_RULES.purpose[pno]?.vendorExceptions?.includes(modName)) { + return true; + } + const {purpose, vendor} = getConsent(consentData, 'purpose', pno, gvlId); + if (purpose && (vendor || ACTIVE_RULES.purpose[pno]?.softVendorExceptions?.includes(modName))) { + return true; + } + } + return false; + } -export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); + const defaultBehavior = gdprRule('2-10', check2to10Consent, eidsBlocked); + const p4Behavior = singlePurposeGdprRule(4, eidsBlocked); + return function () { + const fn = ACTIVE_RULES.purpose[4]?.eidsRequireP4Consent ? p4Behavior : defaultBehavior; + return fn.apply(this, arguments); + }; +})()); -export const ufpdRule = gdprRule(4, () => purpose4Rule, ufpdBlocked); +export const transmitPreciseGeoRule = gdprRule('Special Feature 1', (cd, modName, gvlId) => validateRules(ACTIVE_RULES.feature[1], cd, modName, gvlId), geoBlocked); /** * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. @@ -250,65 +288,55 @@ function emitTCF2FinalResults() { biddersBlocked: formatSet(biddersBlocked), analyticsBlocked: formatSet(analyticsBlocked), ufpdBlocked: formatSet(ufpdBlocked), + eidsBlocked: formatSet(eidsBlocked), + geoBlocked: formatSet(geoBlocked) }; events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); - [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked].forEach(el => el.clear()); + [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked, eidsBlocked, geoBlocked].forEach(el => el.clear()); } events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); -function hasPurpose(purposeNo) { - const pname = TCF2[`purpose${purposeNo}`].name; - return (rule) => rule.purpose === pname; -} - /** * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object */ export function setEnforcementConfig(config) { - const rules = deepAccess(config, 'gdpr.rules'); + let rules = deepAccess(config, 'gdpr.rules'); if (!rules) { logWarn('TCF2: enforcing P1 and P2 by default'); - enforcementRules = DEFAULT_RULES; - } else { - enforcementRules = rules; } + rules = Object.fromEntries((rules || []).map(r => [r.purpose, r])); strictStorageEnforcement = !!deepAccess(config, STRICT_STORAGE_ENFORCEMENT); - purpose1Rule = find(enforcementRules, hasPurpose(1)); - purpose2Rule = find(enforcementRules, hasPurpose(2)); - purpose4Rule = find(enforcementRules, hasPurpose(4)) - purpose7Rule = find(enforcementRules, hasPurpose(7)); - - if (!purpose1Rule) { - purpose1Rule = DEFAULT_RULES[0]; - } - - if (!purpose2Rule) { - purpose2Rule = DEFAULT_RULES[1]; - } + Object.entries(CONFIGURABLE_RULES).forEach(([name, opts]) => { + ACTIVE_RULES[opts.type][opts.id] = rules[name] ?? opts.default; + }); if (!hooksAdded) { - if (purpose1Rule) { + if (ACTIVE_RULES.purpose[1] != null) { hooksAdded = true; RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); } - if (purpose2Rule) { + if (ACTIVE_RULES.purpose[2] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } - if (purpose4Rule) { + if (ACTIVE_RULES.purpose[4] != null) { RULE_HANDLES.push( registerActivityControl(ACTIVITY_TRANSMIT_UFPD, RULE_NAME, ufpdRule), registerActivityControl(ACTIVITY_ENRICH_UFPD, RULE_NAME, ufpdRule) ); } - if (purpose7Rule) { + if (ACTIVE_RULES.purpose[7] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)); } + if (ACTIVE_RULES.feature[1] != null) { + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_PRECISE_GEO, RULE_NAME, transmitPreciseGeoRule)); + } + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_EIDS, RULE_NAME, transmitEidsRule)); } } diff --git a/modules/genericAnalyticsAdapter.js b/modules/genericAnalyticsAdapter.js index b52cb7e5464..7f721863912 100644 --- a/modules/genericAnalyticsAdapter.js +++ b/modules/genericAnalyticsAdapter.js @@ -1,6 +1,6 @@ import AnalyticsAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import {prefixLog, isPlainObject} from '../src/utils.js'; -import * as CONSTANTS from '../src/constants.json'; +import {has as hasEvent} from '../src/events.js'; import adapterManager from '../src/adapterManager.js'; import {ajaxBuilder} from '../src/ajax.js'; @@ -48,12 +48,12 @@ export function GenericAnalytics() { return false; } for (const [event, handler] of Object.entries(options.events)) { - if (!CONSTANTS.EVENTS.hasOwnProperty(event)) { + if (!hasEvent(event)) { logWarn(`options.events.${event} does not match any known Prebid event`); - if (typeof handler !== 'function') { - logError(`options.events.${event} must be a function`); - return false; - } + } + if (typeof handler !== 'function') { + logError(`options.events.${event} must be a function`); + return false; } } } diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 6f910632fbc..0b0d9027c03 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -17,9 +17,16 @@ import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; +import { generateUUID, createInvisibleIframe, insertElement, isEmpty, logError } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import { loadExternalScript } from '../src/adloader.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ /** @type {string} */ const SUBMODULE_NAME = 'geoedge'; @@ -33,9 +40,13 @@ const PV_ID = generateUUID(); /** @type {string} */ const HOST_NAME = 'https://rumcdn.geoedge.be'; /** @type {string} */ -const FILE_NAME = 'grumi.js'; +const FILE_NAME_CLIENT = 'grumi.js'; +/** @type {string} */ +const FILE_NAME_INPAGE = 'grumi-ip.js'; +/** @type {function} */ +export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_CLIENT}`; /** @type {function} */ -export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME}`; +export let getInPageUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_INPAGE}`; /** @type {string} */ export let wrapper /** @type {boolean} */; @@ -45,7 +56,7 @@ let preloaded; /** * fetches the creative wrapper - * @param {function} sucess - success callback + * @param {function} success - success callback */ export function fetchWrapper(success) { if (wrapperReady) { @@ -63,17 +74,38 @@ export function setWrapper(responseText) { wrapper = responseText; } +export function getInitialParams(key) { + let refererInfo = getRefererInfo(); + let params = { + wver: 'pbjs', + wtype: 'pbjs-module', + key, + meta: { + topUrl: refererInfo.page + }, + site: refererInfo.domain, + pimp: PV_ID, + fsRan: true, + frameApi: true + }; + return params; +} + +export function markAsLoaded() { + preloaded = true; +} + /** * preloads the client - * @param {string} key + * @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); + let iframe = createInvisibleIframe(); + iframe.id = 'grumiFrame'; + insertElement(iframe); + iframe.contentWindow.grumi = getInitialParams(key); + let url = getClientUrl(key); + loadExternalScript(url, SUBMODULE_NAME, markAsLoaded, iframe.contentDocument); } /** @@ -97,7 +129,7 @@ export function wrapHtml(wrapper, html) { * @param {string} key * @return {Object} */ -function getMacros(bid, key) { +export function getMacros(bid, key) { return { '${key}': key, '%%ADUNIT%%': bid.adUnitCode, @@ -110,7 +142,9 @@ function getMacros(bid, key) { '%_hbadomains': bid.meta && bid.meta.advertiserDomains, '%%PATTERN:hb_pb%%': bid.pbHg, '%%SITE%%': location.hostname, - '%_pimp%': PV_ID + '%_pimp%': PV_ID, + '%_hbCpm!': bid.cpm, + '%_hbCurrency!': bid.currency }; } @@ -177,7 +211,8 @@ function isSupportedBidder(bidder, paramsBidders) { function shouldWrap(bid, params) { let supportedBidder = isSupportedBidder(bid.bidderCode, params.bidders); let donePreload = params.wap ? preloaded : true; - return wrapperReady && supportedBidder && donePreload; + let isGPT = params.gpt; + return wrapperReady && supportedBidder && donePreload && !isGPT; } function conditionallyWrap(bidResponse, config, userConsent) { @@ -187,31 +222,55 @@ function conditionallyWrap(bidResponse, config, userConsent) { } } +function isBillingMessage(data, params) { + return data.key === params.key && data.impression; +} + /** - * Fire billable events for applicable bids + * Fire billable events when our client sends a message + * Messages will be sent only when: + * a. applicable bids are wrapped + * b. our code laoded and executed sucesfully */ function fireBillableEventsForApplicableBids(params) { - events.on(CONSTANTS.EVENTS.BID_WON, function (winningBid) { - if (shouldWrap(winningBid, params)) { + window.addEventListener('message', function (message) { + let data = message.data; + if (isBillingMessage(data, params)) { + let winningBid = auctionManager.findBidByAdId(data.adId); events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { vendor: SUBMODULE_NAME, - billingId: generateUUID(), - type: 'impression', - transactionId: winningBid.transactionId, - auctionId: winningBid.auctionId, - bidId: winningBid.requestId + billingId: data.impressionId, + type: winningBid ? 'impression' : data.type, + transactionId: winningBid?.transactionId || data.transactionId, + auctionId: winningBid?.auctionId || data.auctionId, + bidId: winningBid?.requestId || data.requestId }); } }); } +/** + * Loads Geoedge in page script that monitors all ad slots created by GPT + * @param {Object} params + */ +function setupInPage(params) { + window.grumi = params; + window.grumi.fromPrebid = true; + loadExternalScript(getInPageUrl(params.key), SUBMODULE_NAME); +} + 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); + if (params.gpt) { + setupInPage(params); + } else { + fetchWrapper(setWrapper); + preloadClient(params.key); + } fireBillableEventsForApplicableBids(params); return true; } @@ -219,17 +278,12 @@ function init(config, userConsent) { /** @type {RtdSubmodule} */ export const geoedgeSubmodule = { /** - * used to link submodule with realTimeData - * @type {string} - */ + * used to link submodule with realTimeData + * @type {string} + */ name: SUBMODULE_NAME, init, onBidResponseEvent: conditionallyWrap }; -export function beforeInit() { - fetchWrapper(setWrapper); - submodule('realTimeData', geoedgeSubmodule); -} - -beforeInit(); +submodule('realTimeData', geoedgeSubmodule); diff --git a/modules/geoedgeRtdProvider.md b/modules/geoedgeRtdProvider.md index 5414606612c..cdf913b8893 100644 --- a/modules/geoedgeRtdProvider.md +++ b/modules/geoedgeRtdProvider.md @@ -5,7 +5,7 @@ Module Type: Rtd Provider Maintainer: guy.books@geoedge.com The Geoedge Realtime module lets publishers block bad ads such as automatic redirects, malware, offensive creatives and landing pages. -To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and cutomer key. +To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and customer key. ## Integration @@ -49,6 +49,7 @@ Parameters details: |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. | +|params.gpt |Boolean |Wrap all GPT ad slots |Optional, defaults to `false`. Set to `true` if you want to monitor all Google Publisher Tag ad slots, regaedless if the winning bid comes from Prebid or Google Ad Manager (Direct, Adx, Adesnse, Open Bidding, etc). | ## Example diff --git a/modules/geolocationRtdProvider.js b/modules/geolocationRtdProvider.js new file mode 100644 index 00000000000..6bfed7ee934 --- /dev/null +++ b/modules/geolocationRtdProvider.js @@ -0,0 +1,65 @@ +import {submodule} from '../src/hook.js'; +import {isFn, logError, deepAccess, deepSetValue, logInfo, logWarn, timestamp} from '../src/utils.js'; +import { ACTIVITY_TRANSMIT_PRECISE_GEO } from '../src/activities/activities.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { isActivityAllowed } from '../src/activities/rules.js'; +import { activityParams } from '../src/activities/activityParams.js'; +import {VENDORLESS_GVLID} from '../src/consentHandler.js'; + +let permissionsAvailable = true; +let geolocation; +function getGeolocationData(requestBidsObject, onDone, providerConfig, userConsent) { + let done = false; + if (!permissionsAvailable) { + logWarn('permission for geolocation receiving was denied'); + return complete() + }; + if (!isActivityAllowed(ACTIVITY_TRANSMIT_PRECISE_GEO, activityParams(MODULE_TYPE_RTD, 'geolocation'))) { + logWarn('permission for geolocation receiving was denied by CMP'); + return complete() + }; + const requestPermission = deepAccess(providerConfig, 'params.requestPermission') === true; + navigator.permissions.query({ + name: 'geolocation', + }).then(permission => { + if (permission.state !== 'granted' && !requestPermission) return complete(); + navigator.geolocation.getCurrentPosition(geo => { + geolocation = geo; + complete(); + }); + }); + function complete() { + if (done) return; + done = true; + if (geolocation) { + deepSetValue(requestBidsObject, 'ortb2Fragments.global.device.geo', { + lat: geolocation.coords.latitude, + lon: geolocation.coords.longitude, + lastfix: Math.round((timestamp() - geolocation.timestamp) / 1000), + type: 1 + }); + logInfo('geolocation was successfully received ', requestBidsObject.ortb2Fragments.global.device.geo) + } + onDone(); + } +} +function init(moduleConfig) { + geolocation = void 0; + if (!isFn(navigator?.permissions?.query) || !isFn(navigator?.geolocation?.getCurrentPosition || !navigator?.permissions?.query)) { + logError('geolocation is not defined'); + permissionsAvailable = false; + } else { + permissionsAvailable = true; + } + return permissionsAvailable; +} +export const geolocationSubmodule = { + name: 'geolocation', + gvlid: VENDORLESS_GVLID, + getBidRequestData: getGeolocationData, + init: init, +}; +function registerSubModule() { + submodule('realTimeData', geolocationSubmodule); +} +registerSubModule(); diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 25322d81f9b..a8888893333 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,6 +1,11 @@ -import { getBidIdParameter, isFn, isInteger } from '../src/utils.js'; +import {getBidIdParameter, isFn, isInteger} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'getintent'; const IS_NET_REVENUE = true; const BID_HOST = 'px.adhigh.net'; @@ -38,7 +43,7 @@ export const spec = { * * @param {BidRequest} bid The bid to validate. * @return {boolean} True if this is a valid bid, and false otherwise. - * */ + */ isBidRequestValid: function(bid) { return !!(bid && bid.params && bid.params.pid && bid.params.tid); }, @@ -106,9 +111,9 @@ function buildUrl(bid) { /** * Builds GI bid request from BidRequest. * - * @param {BidRequest} bidRequest. - * @return {object} GI bid request. - * */ + * @param {BidRequest} bidRequest + * @return {object} GI bid request + */ function buildGiBidRequest(bidRequest) { let giBidRequest = { bid_id: bidRequest.bidId, @@ -191,7 +196,7 @@ function addOptional(params, request, props) { /** * @param {String} s The string representing a size (e.g. "300x250"). * @return {Number[]} An array with two elements: [width, height] (e.g.: [300, 250]). - * */ + */ function parseSize(s) { return s.split('x').map(Number); } @@ -200,7 +205,7 @@ function parseSize(s) { * @param {Array} sizes An array of sizes/numbers to be joined into single string. * May be an array (e.g. [300, 250]) or array of arrays (e.g. [[300, 250], [640, 480]]. * @return {String} The string with sizes, e.g. array of sizes [[50, 50], [80, 80]] becomes "50x50,80x80" string. - * */ + */ function produceSize (sizes) { function sizeToStr(s) { if (Array.isArray(s) && s.length === 2 && isInteger(s[0]) && isInteger(s[1])) { diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index 91ed5c9b3fb..ef19a097062 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.js @@ -2,6 +2,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'gjirafa'; const ENDPOINT_URL = 'https://central.gjirafa.com/bid'; const DIMENSION_SEPARATOR = 'x'; @@ -26,7 +34,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { @@ -116,11 +125,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index 8c90d0cccfe..d7af51f7312 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,17 +1,26 @@ import { createTrackPixelHtml, deepAccess, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, getDNT, getWindowTop, isEmpty, - logError, - tryAppendQueryString + logError } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'gmossp'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; @@ -33,7 +42,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { diff --git a/modules/gnetBidAdapter.js b/modules/gnetBidAdapter.js index 38e96c183b9..1bcc774e351 100644 --- a/modules/gnetBidAdapter.js +++ b/modules/gnetBidAdapter.js @@ -4,6 +4,13 @@ import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'gnet'; const ENDPOINT = 'https://service.gnetrtb.com/api'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -25,7 +32,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 4768931950c..9f9913b7023 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -1,15 +1,9 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -25,13 +19,20 @@ import { import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { APPNEXUS_CATEGORY_MAPPING } from '../libraries/categoryTranslationMapping/index.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'goldbach'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -880,9 +881,7 @@ function bidToTag(bid) { tag['banner_frameworks'] = bid.params.frameworks; } - // TODO: why does this need to iterate through every ad unit? - let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); - if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { + if (bid.mediaTypes?.banner) { tag.ad_types.push(BANNER); } @@ -971,7 +970,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -997,7 +996,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/goldfishAdsRtdProvider.js b/modules/goldfishAdsRtdProvider.js new file mode 100755 index 00000000000..c595e361968 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.js @@ -0,0 +1,198 @@ +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess } from '../src/utils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +export const MODULE_NAME = 'goldfishAdsRtd'; +export const MODULE_TYPE = 'realTimeData'; +export const ENDPOINT_URL = 'https://prebid.goldfishads.com/iab-segments'; +export const DATA_STORAGE_KEY = 'goldfishads_data'; +export const DATA_STORAGE_TTL = 1800 * 1000// TTL in seconds + +export const ADAPTER_VERSION = '1.0'; + +export const storage = getStorageManager({ + gvlid: null, + moduleName: MODULE_NAME, + moduleType: MODULE_TYPE, +}); + +/** + * + * @param {{response: string[]} } response + * @returns + */ +export const manageCallbackResponse = (response) => { + try { + const foo = JSON.parse(response.response); + if (!Array.isArray(foo)) throw new Error('Invalid response'); + const enrichedResponse = { + ext: { + segtax: 4 + }, + segment: foo.map((segment) => { return { id: segment } }), + }; + const output = { + name: 'goldfishads.com', + ...enrichedResponse, + }; + return output; + } catch (e) { + throw e; + }; +}; + +/** + * @param {string} key + * @returns { Promise<{name: 'goldfishads.com', ext: { segtag: 4 }, segment: string[]}> } + */ + +const getTargetingDataFromApi = (key) => { + return new Promise((resolve, reject) => { + const requestOptions = { + customHeaders: { + 'Accept': 'application/json' + } + } + const callbacks = { + success(responseText, response) { + try { + const output = manageCallbackResponse(response); + resolve(output); + } catch (e) { + reject(e); + } + }, + error(error) { + reject(error); + } + }; + ajax(`${ENDPOINT_URL}?key=${key}`, callbacks, null, requestOptions) + }) +}; + +/** + * @returns {{ + * name: 'golfishads.com', + * ext: { segtax: 4}, + * segment: string[] + * } | null } + */ +export const getStorageData = () => { + const now = new Date(); + const data = storage.getDataFromLocalStorage(DATA_STORAGE_KEY); + if (data === null) return null; + try { + const foo = JSON.parse(data); + if (now.getTime() > foo.expiry) return null; + return foo.targeting; + } catch (e) { + return null; + } +}; + +/** + * @param { { key: string } } payload + * @returns {Promise<{ + * name: string, + * ext: { segtax: 4}, + * segment: string[] + * }> | null + * } + */ + +const getTargetingData = (payload) => new Promise((resolve) => { + const targeting = getStorageData(); + if (targeting === null) { + getTargetingDataFromApi(payload.key) + .then((response) => { + const now = new Date() + const data = { + targeting: response, + expiry: now.getTime() + DATA_STORAGE_TTL, + }; + storage.setDataInLocalStorage(DATA_STORAGE_KEY, JSON.stringify(data)); + resolve(response); + }) + .catch((e) => { + resolve(null); + }); + } else { + resolve(targeting); + } +}) + +/** + * + * @param {*} config + * @param {*} userConsent + * @returns {boolean} + */ + +const init = (config, userConsent) => { + if (!config.params || !config.params.key) return false; + // return { type: (typeof config.params.key === 'string') }; + if (!(typeof config.params.key === 'string')) return false; + return true; +}; + +/** + * + * @param {{ + * name: string, + * ext: { segtax: 4}, + * segment: {id: string}[] + * } | null } userData + * @param {*} reqBidsConfigObj + * @returns + */ +export const updateUserData = (userData, reqBidsConfigObj) => { + if (userData === null) return; + const bidders = ['appnexus', 'rubicon', 'nexx360']; + for (let i = 0; i < bidders.length; i++) { + const bidderCode = bidders[i]; + const originalConfig = deepAccess(reqBidsConfigObj, `ortb2Fragments.bidder[${bidderCode}].user.data`) || []; + const userConfig = [ + ...originalConfig, + userData, + ]; + reqBidsConfigObj.ortb2Fragments = reqBidsConfigObj.ortb2Fragments || {}; + reqBidsConfigObj.ortb2Fragments.bidder = reqBidsConfigObj.ortb2Fragments.bidder || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data || userConfig; + } + return reqBidsConfigObj; +} + +/** + * + * @param {*} reqBidsConfigObj + * @param {*} callback + * @param {*} moduleConfig + * @param {*} userConsent + * @returns {void} + */ +const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + const payload = { + key: moduleConfig.params.key, + }; + getTargetingData(payload) + .then((userData) => { + updateUserData(userData, reqBidsConfigObj); + callback(); + }); +}; + +/** @type {RtdSubmodule} */ +export const goldfishAdsSubModule = { + name: MODULE_NAME, + init, + getBidRequestData, +}; + +submodule(MODULE_TYPE, goldfishAdsSubModule); diff --git a/modules/goldfishAdsRtdProvider.md b/modules/goldfishAdsRtdProvider.md new file mode 100755 index 00000000000..4625c9a7988 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.md @@ -0,0 +1,48 @@ +# Goldfish Ads Real-time Data Submodule + +## Overview + + Module Name: Goldfish Ads Rtd Provider + Module Type: Rtd Provider + Maintainer: keith@goldfishads.com + +## Description + +This RTD module provides access to the Goldfish Ads Geograph, which leverages geographic and temporal data on a privcay-first platform. This module works without using cookies, PII, emails, or device IDs across all website traffic, including unauthenticated users, and adds audience data into bid requests to increase scale and yields. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,goldfishAdsRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Goldfish Ads RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Goldfish Ads RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'goldfishAds', + waitForIt: true, + params: { + key: 'testkey' + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'goldfishAds' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.key | String | Your key id issued by Goldfish Ads | | diff --git a/modules/gothamadsBidAdapter.js b/modules/gothamadsBidAdapter.js index 9f44a54460f..ab59c6febec 100644 --- a/modules/gothamadsBidAdapter.js +++ b/modules/gothamadsBidAdapter.js @@ -4,6 +4,11 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'gothamads'; const ACCOUNTID_MACROS = '[account_id]'; const URL_ENDPOINT = `https://us-e-node1.gothamads.com/bid?pass=${ACCOUNTID_MACROS}&integration=prebidjs`; diff --git a/modules/gppControl_usstates.js b/modules/gppControl_usstates.js new file mode 100644 index 00000000000..bc2b434e085 --- /dev/null +++ b/modules/gppControl_usstates.js @@ -0,0 +1,176 @@ +import {config} from '../src/config.js'; +import {setupRules} from '../libraries/mspa/activityControls.js'; +import {deepSetValue, prefixLog} from '../src/utils.js'; + +const FIELDS = { + Version: 0, + Gpc: 0, + SharingNotice: 0, + SaleOptOutNotice: 0, + SharingOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 0, + SensitiveDataProcessingOptOutNotice: 0, + SensitiveDataLimitUseNotice: 0, + SaleOptOut: 0, + SharingOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: 12, + KnownChildSensitiveDataConsents: 2, + PersonalDataConsents: 0, + MspaCoveredTransaction: 0, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, +}; + +/** + * Generate a normalization function for converting US state strings to the usnat format. + * + * Scalar fields are copied over if they exist in the input (state) data, or set to null otherwise. + * List fields are also copied, but forced to the "correct" length (by truncating or padding with nulls); + * additionally, elements within them can be moved around using the `move` argument. + * + * @param {Array[String]} nullify? list of fields to force to null + * @param {{}} move? Map from list field name to an index remapping for elements within that field (using 1 as the first index). + * For example, {SensitiveDataProcessing: {1: 2, 2: [1, 3]}} means "rearrange SensitiveDataProcessing by moving + * the first element to the second position, and the second element to both the first and third position." + * @param {({}, {}) => void} fn? an optional function to run once all the processing described above is complete; + * it's passed two arguments, the original (state) data, and its normalized (usnat) version. + * @param fields + * @returns {function({}): {}} + */ +export function normalizer({nullify = [], move = {}, fn}, fields = FIELDS) { + move = Object.fromEntries(Object.entries(move).map(([k, map]) => [k, + Object.fromEntries(Object.entries(map) + .map(([k, v]) => [k, Array.isArray(v) ? v : [v]]) + .map(([k, v]) => [--k, v.map(el => --el)]) + )]) + ); + return function (cd) { + const norm = Object.fromEntries(Object.entries(fields) + .map(([field, len]) => { + let val = null; + if (len > 0) { + val = Array(len).fill(null); + if (Array.isArray(cd[field])) { + const remap = move[field] || {}; + const done = []; + cd[field].forEach((el, i) => { + const [dest, moved] = remap.hasOwnProperty(i) ? [remap[i], true] : [[i], false]; + dest.forEach(d => { + if (d < len && !done.includes(d)) { + val[d] = el; + moved && done.push(d); + } + }); + }); + } + } else if (cd[field] != null) { + val = Array.isArray(cd[field]) ? null : cd[field]; + } + return [field, val]; + })); + nullify.forEach(path => deepSetValue(norm, path, null)); + fn && fn(cd, norm); + return norm; + }; +} + +function scalarMinorsAreChildren(original, normalized) { + normalized.KnownChildSensitiveDataConsents = original.KnownChildSensitiveDataConsents === 0 ? [0, 0] : [1, 1]; +} + +export const NORMALIZATIONS = { + // normalization rules - convert state consent into usnat consent + // https://docs.prebid.org/features/mspa-usnat.html + 7: (consent) => consent, + 8: normalizer({ + move: { + SensitiveDataProcessing: { + 1: 9, + 2: 10, + 3: 8, + 4: [1, 2], + 5: 12, + 8: 3, + 9: 4, + } + }, + fn(original, normalized) { + if (original.KnownChildSensitiveDataConsents.some(el => el !== 0)) { + normalized.KnownChildSensitiveDataConsents = [1, 1]; + } + } + }), + 9: normalizer({fn: scalarMinorsAreChildren}), + 10: normalizer({fn: scalarMinorsAreChildren}), + 11: normalizer({ + move: { + SensitiveDataProcessing: { + 3: 4, + 4: 5, + 5: 3, + } + }, + fn: scalarMinorsAreChildren + }), + 12: normalizer({ + fn(original, normalized) { + const cc = original.KnownChildSensitiveDataConsents; + let repl; + if (!cc.some(el => el !== 0)) { + repl = [0, 0]; + } else if (cc[1] === 2 && cc[2] === 2) { + repl = [2, 1]; + } else { + repl = [1, 1]; + } + normalized.KnownChildSensitiveDataConsents = repl; + } + }) +}; + +export const DEFAULT_SID_MAPPING = { + 8: 'usca', + 9: 'usva', + 10: 'usco', + 11: 'usut', + 12: 'usct' +}; + +export const getSections = (() => { + const allSIDs = Object.keys(DEFAULT_SID_MAPPING).map(Number); + return function ({sections = {}, sids = allSIDs} = {}) { + return sids.map(sid => { + const logger = prefixLog(`Cannot set up MSPA controls for SID ${sid}:`); + const ov = sections[sid] || {}; + const normalizeAs = ov.normalizeAs || sid; + if (!NORMALIZATIONS.hasOwnProperty(normalizeAs)) { + logger.logError(`no normalization rules are known for SID ${normalizeAs}`) + return; + } + const api = ov.name || DEFAULT_SID_MAPPING[sid]; + if (typeof api !== 'string') { + logger.logError(`cannot determine GPP section name`) + return; + } + return [ + api, + [sid], + NORMALIZATIONS[normalizeAs] + ] + }).filter(el => el != null); + } +})(); + +const handles = []; + +config.getConfig('consentManagement', (cfg) => { + const gppConf = cfg.consentManagement?.gpp; + if (gppConf) { + while (handles.length) { + handles.pop()(); + } + getSections(gppConf?.mspa || {}) + .forEach(([api, sids, normalize]) => handles.push(setupRules(api, sids, normalize))); + } +}); diff --git a/modules/gptPreAuction.js b/modules/gptPreAuction.js index 71884235b38..bf5b4a55dbb 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -1,4 +1,11 @@ -import {deepAccess, isAdUnitCodeMatchingSlot, isGptPubadsDefined, logInfo, pick} from '../src/utils.js'; +import { + deepAccess, + isAdUnitCodeMatchingSlot, + isGptPubadsDefined, + logInfo, + pick, + deepSetValue +} from '../src/utils.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; import {find} from '../src/polyfill.js'; @@ -15,7 +22,8 @@ export const appendGptSlots = adUnits => { } const adUnitMap = adUnits.reduce((acc, adUnit) => { - acc[adUnit.code] = adUnit; + acc[adUnit.code] = acc[adUnit.code] || []; + acc[adUnit.code].push(adUnit); return acc; }, {}); @@ -25,15 +33,13 @@ export const appendGptSlots = adUnits => { : isAdUnitCodeMatchingSlot(slot)); if (matchingAdUnitCode) { - const adUnit = adUnitMap[matchingAdUnitCode]; - adUnit.ortb2Imp = adUnit.ortb2Imp || {}; - adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; - adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; - - const context = adUnit.ortb2Imp.ext.data; - context.adserver = context.adserver || {}; - context.adserver.name = 'gam'; - context.adserver.adslot = sanitizeSlotPath(slot.getAdUnitPath()); + const adserver = { + name: 'gam', + adslot: sanitizeSlotPath(slot.getAdUnitPath()) + }; + adUnitMap[matchingAdUnitCode].forEach((adUnit) => { + deepSetValue(adUnit, 'ortb2Imp.ext.data.adserver', Object.assign({}, adUnit.ortb2Imp?.ext?.data?.adserver, adserver)); + }); } }); }; diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index 70031ebd06e..cc02c6a103e 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -16,16 +16,16 @@ export const cookieKey = 'gravitompId'; export const gravitoIdSystemSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * performs action to obtain id - * @function - * @returns { {id: {gravitompId: string}} | undefined } - */ + * performs action to obtain id + * @function + * @returns { {id: {gravitompId: string}} | undefined } + */ getId: function() { const newId = storage.getCookie(cookieKey); if (!newId) { @@ -38,11 +38,11 @@ export const gravitoIdSystemSubmodule = { }, /** - * decode the stored id value for passing to bid requests - * @function - * @param { {gravitompId: string} } value - * @returns { {gravitompId: {string} } | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { {gravitompId: string} } value + * @returns { {gravitompId: {string} } | undefined } + */ decode: function(value) { if (value && typeof value === 'object') { var result = {}; diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index edc0c9c6c5c..b881e868bf3 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -2,18 +2,20 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {deepClone, logError, logInfo} from '../src/utils.js'; +import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils.js'; const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '1.0.0'; +export const ANALYTICS_VERSION = '2.2.0'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; const { EVENTS: { + AUCTION_INIT, AUCTION_END, BID_TIMEOUT, + BILLABLE_EVENT, } } = CONSTANTS; @@ -25,28 +27,64 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const parseBidderCode = function (bid) { - let bidderCode = bid.bidderCode || bid.bidder; - return bidderCode.toLowerCase(); -}; +export const isSampled = function(greenbidsId, samplingRate, exploratorySamplingSplit) { + if (samplingRate < 0 || samplingRate > 1) { + logWarn('Sampling rate must be between 0 and 1'); + return true; + } + const exploratorySamplingRate = samplingRate * exploratorySamplingSplit; + const throttledSamplingRate = samplingRate * (1.0 - exploratorySamplingSplit); + const hashInt = parseInt(greenbidsId.slice(-4), 16); + const isPrimarySampled = hashInt < exploratorySamplingRate * (0xFFFF + 1); + if (isPrimarySampled) return true; + const isExtraSampled = hashInt >= (1 - throttledSamplingRate) * (0xFFFF + 1); + return isExtraSampled; +} export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { cachedAuctions: {}, + exploratorySamplingSplit: 0.9, initConfig(config) { + analyticsOptions.options = deepClone(config.options); /** * Required option: pbuid * @type {boolean} */ - analyticsOptions.options = deepClone(config.options); - if (typeof config.options.pbuid !== 'string' || config.options.pbuid.length < 1) { + if (typeof analyticsOptions.options.pbuid !== 'string' || analyticsOptions.options.pbuid.length < 1) { logError('"options.pbuid" is required.'); return false; } + /** + * Deprecate use of integerated 'sampling' config + * replace by greenbidsSampling + */ + if (typeof analyticsOptions.options.sampling === 'number') { + logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.'); + analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling; + } + + /** + * Discourage unsampled analytics + */ + if (typeof analyticsOptions.options.greenbidsSampling !== 'number' || analyticsOptions.options.greenbidsSampling >= 1) { + logWarn('"options.greenbidsSampling" is not set or >=1, using this analytics module unsampled is discouraged.'); + analyticsOptions.options.greenbidsSampling = 1; + } + + /** + * Add optional debug parameter to override exploratorySamplingSplit + */ + if (typeof analyticsOptions.options.exploratorySamplingSplit === 'number') { + logInfo('Greenbids Analytics: Overriding "exploratorySamplingSplit".'); + this.exploratorySamplingSplit = analyticsOptions.options.exploratorySamplingSplit; + } + analyticsOptions.pbuid = config.options.pbuid analyticsOptions.server = ANALYTICS_SERVER; + return true; }, sendEventMessage(endPoint, data) { @@ -57,13 +95,16 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }); }, createCommonMessage(auctionId) { + const cachedAuction = this.getCachedAuction(auctionId); return { version: ANALYTICS_VERSION, auctionId: auctionId, referrer: window.location.href, - sampling: analyticsOptions.options.sampling, + sampling: analyticsOptions.options.greenbidsSampling, prebid: '$prebid.version$', + greenbidsId: cachedAuction.greenbidsId, pbuid: analyticsOptions.pbuid, + billingId: cachedAuction.billingId, adUnits: [], }; }, @@ -96,22 +137,24 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER } } }, - createBidMessage(auctionEndArgs, timeoutBids) { - logInfo(auctionEndArgs) + createBidMessage(auctionEndArgs) { const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; + const cachedAuction = this.getCachedAuction(auctionId); const message = this.createCommonMessage(auctionId); + const timeoutBids = cachedAuction.timeoutBids || []; message.auctionElapsed = (auctionEnd - timestamp); adUnits.forEach((adUnit) => { - const adUnitCode = adUnit.code.toLowerCase(); + const adUnitCode = adUnit.code?.toLowerCase() || 'unknown_adunit_code'; message.adUnits.push({ code: adUnitCode, mediaTypes: { - ...(adUnit.mediaTypes.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, - ...(adUnit.mediaTypes.video !== undefined) && {video: adUnit.mediaTypes.video}, - ...(adUnit.mediaTypes.native !== undefined) && {native: adUnit.mediaTypes.native} + ...(adUnit.mediaTypes?.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, + ...(adUnit.mediaTypes?.video !== undefined) && {video: adUnit.mediaTypes.video}, + ...(adUnit.mediaTypes?.native !== undefined) && {native: adUnit.mediaTypes.native} }, + ortb2Imp: adUnit.ortb2Imp || {}, bidders: [], }); }); @@ -129,13 +172,26 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER getCachedAuction(auctionId) { this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || { timeoutBids: [], + greenbidsId: null, + billingId: null, + isSampled: true, }; return this.cachedAuctions[auctionId]; }, + handleAuctionInit(auctionInitArgs) { + const cachedAuction = this.getCachedAuction(auctionInitArgs.auctionId); + try { + cachedAuction.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId; + } catch (e) { + logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); + cachedAuction.greenbidsId = generateUUID(); + } + cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling, this.exploratorySamplingSplit); + }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); this.sendEventMessage('/', - this.createBidMessage(auctionEndArgs, cachedAuction.timeoutBids) + this.createBidMessage(auctionEndArgs, cachedAuction) ); }, handleBidTimeout(timeoutBids) { @@ -144,14 +200,34 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER cachedAuction.timeoutBids.push(bid); }); }, + handleBillable(billableArgs) { + const cachedAuction = this.getCachedAuction(billableArgs.auctionId); + /* Filter Greenbids Billable Events only */ + if (billableArgs.vendor === 'greenbidsRtdProvider') { + cachedAuction.billingId = billableArgs.billingId || 'unknown_billing_id'; + } + }, track({eventType, args}) { - switch (eventType) { - case BID_TIMEOUT: - this.handleBidTimeout(args); - break; - case AUCTION_END: - this.handleAuctionEnd(args); - break; + try { + if (eventType === AUCTION_INIT) { + this.handleAuctionInit(args); + } + + if (this.getCachedAuction(args?.auctionId)?.isSampled ?? true) { + switch (eventType) { + case BID_TIMEOUT: + this.handleBidTimeout(args); + break; + case AUCTION_END: + this.handleAuctionEnd(args); + break; + case BILLABLE_EVENT: + this.handleBillable(args); + break; + } + } + } catch (e) { + logWarn('There was an error handling event ' + eventType); } }, getAnalyticsOptions() { @@ -163,6 +239,10 @@ greenbidsAnalyticsAdapter.originEnableAnalytics = greenbidsAnalyticsAdapter.enab greenbidsAnalyticsAdapter.enableAnalytics = function(config) { this.initConfig(config); + if (typeof config.options.sampling === 'number') { + // Set sampling to 1 to prevent prebid analytics integrated sampling to happen + config.options.sampling = 1; + } logInfo('loading greenbids analytics'); greenbidsAnalyticsAdapter.originEnableAnalytics(config); }; diff --git a/modules/greenbidsAnalyticsAdapter.md b/modules/greenbidsAnalyticsAdapter.md index 46e3af2c5e2..1be2c1741ed 100644 --- a/modules/greenbidsAnalyticsAdapter.md +++ b/modules/greenbidsAnalyticsAdapter.md @@ -1,23 +1,24 @@ -# Overview +#### Registration -``` -Module Name: Greenbids Analytics Adapter -Module Type: Analytics Adapter -Maintainer: jb@greenbids.ai -``` +The Greenbids Analytics adapter requires setup and approval from the +Greenbids team. Please reach out to our team for more information [greenbids.ai](https://greenbids.ai). -# Description +#### Analytics Options -Analytics adapter for Greenbids +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-------------|---------|--------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|------------------| +| pbuid | required | The Greenbids Publisher ID | greenbids-publisher-1 | string | +| greenbidsSampling | optional | sampling factor [0-1] (a value of 0.1 will filter 90% of the traffic) | 1.0 | float | -# Test Parameters +### Example Configuration -``` -{ - provider: 'greenbids', - options: { - pbuid: "PBUID_FROM_GREENBIDS" - sampling: 1.0 - } -} -``` +```javascript + pbjs.enableAnalytics({ + provider: 'greenbids', + options: { + pbuid: "greenbids-publisher-1" // please contact Greenbids to get a pbuid for yourself + greenbidsSampling: 1.0 + } + }); +``` \ No newline at end of file diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index ef12326cf18..7fcd163a7c2 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,12 +1,13 @@ -import { logError } from '../src/utils.js'; +import { logError, deepClone, generateUUID, deepSetValue, deepAccess } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; const MODULE_NAME = 'greenbidsRtdProvider'; -const MODULE_VERSION = '1.0.0'; -const ENDPOINT = 'https://europe-west1-greenbids-357713.cloudfunctions.net/partner-selection'; +const MODULE_VERSION = '2.0.0'; +const ENDPOINT = 'https://t.greenbids.ai'; -const auctionInfo = {}; const rtdOptions = {}; function init(moduleConfig) { @@ -16,22 +17,33 @@ function init(moduleConfig) { return false; } else { rtdOptions.pbuid = params?.pbuid; - rtdOptions.targetTPR = params?.targetTPR || 0.99; rtdOptions.timeout = params?.timeout || 200; return true; } } function onAuctionInitEvent(auctionDetails) { - auctionInfo.auctionId = auctionDetails.auctionId; + /* Emitting one billing event per auction */ + let defaultId = 'default_id'; + let greenbidsId = deepAccess(auctionDetails.adUnits[0], 'ortb2Imp.ext.greenbids.greenbidsId', defaultId); + /* greenbids was successfully called so we emit the event */ + if (greenbidsId !== defaultId) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: auctionDetails.auctionId, + vendor: MODULE_NAME + }); + } } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - let promise = createPromise(reqBidsConfigObj); + let greenbidsId = generateUUID(); + let promise = createPromise(reqBidsConfigObj, greenbidsId); promise.then(callback); } -function createPromise(reqBidsConfigObj) { +function createPromise(reqBidsConfigObj, greenbidsId) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { resolve(reqBidsConfigObj); @@ -40,7 +52,7 @@ function createPromise(reqBidsConfigObj) { ENDPOINT, { success: (response) => { - processSuccessResponse(response, timeoutId, reqBidsConfigObj); + processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId); resolve(reqBidsConfigObj); }, error: () => { @@ -48,24 +60,35 @@ function createPromise(reqBidsConfigObj) { resolve(reqBidsConfigObj); }, }, - createPayload(reqBidsConfigObj), - { contentType: 'application/json' } + createPayload(reqBidsConfigObj, greenbidsId), + { + contentType: 'application/json', + customHeaders: { + 'Greenbids-Pbuid': rtdOptions.pbuid + } + } ); }); } -function processSuccessResponse(response, timeoutId, reqBidsConfigObj) { +function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) { clearTimeout(timeoutId); const responseAdUnits = JSON.parse(response); - - updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits); + updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); } -function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) { +function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { adUnits.forEach((adUnit) => { const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); if (matchingAdUnit) { - removeFalseBidders(adUnit, matchingAdUnit); + deepSetValue(adUnit, 'ortb2Imp.ext.greenbids', { + greenbidsId: greenbidsId, + keptInAuction: matchingAdUnit.bidders, + isExploration: matchingAdUnit.isExploration + }); + if (!matchingAdUnit.isExploration) { + removeFalseBidders(adUnit, matchingAdUnit); + } } }); } @@ -85,14 +108,24 @@ function getFalseBidders(bidders) { .map(([bidder]) => bidder); } -function createPayload(reqBidsConfigObj) { +function stripAdUnits(adUnits) { + const stripedAdUnits = deepClone(adUnits); + return stripedAdUnits.map(adUnit => { + adUnit.bids = adUnit.bids.map(bid => { + return { bidder: bid.bidder }; + }); + return adUnit; + }); +} + +function createPayload(reqBidsConfigObj, greenbidsId) { return JSON.stringify({ - auctionId: auctionInfo.auctionId, version: MODULE_VERSION, + ...rtdOptions, referrer: window.location.href, prebid: '$prebid.version$', - rtdOptions: rtdOptions, - adUnits: reqBidsConfigObj.adUnits, + greenbidsId: greenbidsId, + adUnits: stripAdUnits(reqBidsConfigObj.adUnits), }); } @@ -105,6 +138,7 @@ export const greenbidsSubmodule = { findMatchingAdUnit: findMatchingAdUnit, removeFalseBidders: removeFalseBidders, getFalseBidders: getFalseBidders, + stripAdUnits: stripAdUnits, }; submodule('realTimeData', greenbidsSubmodule); diff --git a/modules/greenbidsRtdProvider.md b/modules/greenbidsRtdProvider.md index 85b8f5a7859..ab8105a4537 100644 --- a/modules/greenbidsRtdProvider.md +++ b/modules/greenbidsRtdProvider.md @@ -2,6 +2,7 @@ ``` Module Name: Greenbids RTD Provider +Module Version: 2.0.0 Module Type: RTD Provider Maintainer: jb@greenbids.ai ``` @@ -21,7 +22,6 @@ This module is configured as part of the `realTimeData.dataProviders` object. | `waitForIt ` | required (mandatory true value) | Tells prebid auction to wait for the result of this module | `'true'` | `boolean` | | `params` | required | | | `Object` | | `params.pbuid` | required | The client site id provided by Greenbids. | `'TEST_FROM_GREENBIDS'` | `string` | -| `params.targetTPR` | optional (default 0.95) | Target True positive rate for the throttling model | `0.99` | `[0-1]` | | `params.timeout` | optional (default 200) | Maximum amount of milliseconds allowed for module to finish working (has to be <= to the realTimeData.auctionDelay property) | `200` | `number` | #### Example diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index ee8712b1de3..f7db6d878f1 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -16,9 +16,15 @@ import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'grid'; const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; -const USP_DELETE_DATA_HANDLER = 'https://media.grid.bidswitch.net/uspapi_delete' +const USP_DELETE_DATA_HANDLER = 'https://media.grid.bidswitch.net/uspapi_delete_c2s' const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; @@ -91,7 +97,7 @@ export const spec = { let {bidderRequestId, gdprConsent, uspConsent, timeout, refererInfo, gppConsent} = bidderRequest || {}; const referer = refererInfo ? encodeURIComponent(refererInfo.page) : ''; - const tmax = timeout; + const tmax = parseInt(timeout) || null; const imp = []; const bidsMap = {}; const requests = []; @@ -133,20 +139,13 @@ export const spec = { }; if (ortb2Imp) { if (ortb2Imp.instl) { - impObj.instl = ortb2Imp.instl; + impObj.instl = parseInt(ortb2Imp.instl) || null; } if (ortb2Imp.ext) { + impObj.ext.gpid = ortb2Imp.ext.gpid?.toString() || ortb2Imp.ext.data?.pbadslot?.toString() || ortb2Imp.ext.data?.adserver?.adslot?.toString(); if (ortb2Imp.ext.data) { impObj.ext.data = ortb2Imp.ext.data; - if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { - impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); - } else if (ortb2Imp.ext.data.pbadslot) { - impObj.ext.gpid = ortb2Imp.ext.data.pbadslot.toString(); - } - } - if (ortb2Imp.ext.gpid) { - impObj.ext.gpid = ortb2Imp.ext.gpid.toString(); } } } @@ -362,6 +361,16 @@ export const spec = { request.regs.coppa = 1; } + if (ortb2Regs?.ext?.dsa) { + if (!request.regs) { + request.regs = {ext: {}}; + } + if (!request.regs.ext) { + request.regs.ext = {}; + } + request.regs.ext.dsa = ortb2Regs.ext.dsa; + } + const site = deepAccess(bidderRequest, 'ortb2.site'); if (site) { const pageCategory = [...(site.cat || []), ...(site.pagecat || [])].filter((category) => { @@ -467,20 +476,12 @@ export const spec = { }, ajaxCall: function(url, cb, data, options) { + options.browsingTopics = false; return ajax(url, cb, data, options); }, onDataDeletionRequest: function(data) { - const uids = []; - const aliases = [spec.code, ...spec.aliases.map((alias) => alias.code || alias)]; - data.forEach(({ bids }) => bids && bids.forEach(({ bidder, params }) => { - if (aliases.includes(bidder) && params && params.uid) { - uids.push(params.uid); - } - })); - if (uids.length) { - spec.ajaxCall(USP_DELETE_DATA_HANDLER, () => {}, JSON.stringify({ uids }), {contentType: 'application/json', method: 'POST'}); - } + spec.ajaxCall(USP_DELETE_DATA_HANDLER, null, null, {method: 'GET'}); } }; @@ -492,7 +493,7 @@ export const spec = { */ function _getFloor (mediaTypes, bid) { const curMediaType = mediaTypes.video ? 'video' : 'banner'; - let floor = bid.params.bidFloor || bid.params.floorcpm || 0; + let floor = parseFloat(bid.params.bidFloor || bid.params.floorcpm || 0) || null; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ @@ -541,7 +542,7 @@ function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bid netRevenue: true, ttl: TIME_TO_LIVE, meta: { - advertiserDomains: serverBid.adomain ? serverBid.adomain : [] + advertiserDomains: serverBid.adomain ? serverBid.adomain : [], }, dealId: serverBid.dealid }; @@ -553,6 +554,10 @@ function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bid bidResponse.meta.demandSource = serverBid.ext.bidder.grid.demandSource; } + if (serverBid.ext && serverBid.ext.dsa) { + bidResponse.meta.dsa = serverBid.ext.dsa; + } + if (serverBid.content_type === 'video') { if (serverBid.adm) { bidResponse.vastXml = serverBid.adm; @@ -602,8 +607,8 @@ function createVideoRequest(videoParams, mediaType, bidSizes) { if (!videoData.w || !videoData.h) return; - const minDur = mind || durationRangeSec[0] || videoData.minduration; - const maxDur = maxd || durationRangeSec[1] || videoData.maxduration; + const minDur = mind || durationRangeSec[0] || parseInt(videoData.minduration) || null; + const maxDur = maxd || durationRangeSec[1] || parseInt(videoData.maxduration) || null; if (minDur) { videoData.minduration = minDur; diff --git a/modules/growadvertisingBidAdapter.js b/modules/growadvertisingBidAdapter.js index b9b256dbaff..f6f7867f0fe 100644 --- a/modules/growadvertisingBidAdapter.js +++ b/modules/growadvertisingBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getBidIdParameter, deepAccess, _each, triggerPixel } from '../src/utils.js'; +import {deepAccess, _each, triggerPixel, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index a2ab4ddbfac..5c7cc254f1d 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -13,7 +13,7 @@ import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' -const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb/analytics' +const ENDPOINT_URL = 'https://analytics.gcprivacy.com/v3/pb/analytics' export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); @@ -30,8 +30,8 @@ let bidRequestTimeout = 0; let analyticsType = 'endpoint'; let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType}), { - track({eventType, eventData}) { - eventData = eventData ? JSON.parse(JSON.stringify(eventData)) : {}; + track({eventType, args}) { + let eventData = args ? JSON.parse(JSON.stringify(args)) : {}; let data = {}; if (!trackEvents.includes(eventType)) return; switch (eventType) { @@ -98,6 +98,11 @@ let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType} break; } + case CONSTANTS.EVENTS.NO_BID: { + data = eventData + break; + } + default: return; } @@ -133,12 +138,15 @@ growthCodeAnalyticsAdapter.enableAnalytics = function(conf = {}) { function logToServer() { if (pid === DEFAULT_PID) return; - if (eventQueue.length > 1) { + if (eventQueue.length >= 1) { + // Get the correct GCID + let gcid = localStorage.getItem('gcid') + let data = { session: sessionId, pid: pid, + gcid: gcid, timestamp: Date.now(), - timezoneoffset: new Date().getTimezoneOffset(), url: getRefererInfo().page, referer: document.referrer, events: eventQueue @@ -162,7 +170,7 @@ function sendEvent(event) { eventQueue.push(event); logInfo(MODULE_NAME + 'Analytics Event: ' + event); - if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + if ((event.eventType === CONSTANTS.EVENTS.AUCTION_END) || (event.eventType === CONSTANTS.EVENTS.BID_WON)) { logToServer(); } } diff --git a/modules/growthCodeAnalyticsAdapter.md b/modules/growthCodeAnalyticsAdapter.md index e45cb2e9c62..6625d492ee6 100644 --- a/modules/growthCodeAnalyticsAdapter.md +++ b/modules/growthCodeAnalyticsAdapter.md @@ -21,13 +21,7 @@ pbjs.enableAnalytics({ pid: '', trackEvents: [ 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'noBid', - 'bidWon', - 'bidderDone'] + 'bidWon'] } }); ``` diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index aec49c64fa3..be20ab89130 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -5,80 +5,20 @@ * @requires module:modules/userId */ -import {logError, logInfo, pick, tryAppendQueryString} from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; import { submodule } from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -const MODULE_NAME = 'growthCodeId'; -const GC_DATA_KEY = '_gc_data'; -const GCID_KEY = 'gcid'; -const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb?' - -export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); - /** - * Read GrowthCode data from cookie or local storage - * @param key - * @return {string} + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse */ -export function readData(key) { - try { - let payload - if (storage.cookiesAreEnabled(null)) { - payload = tryParse(storage.getCookie(key, null)) - } - if (storage.hasLocalStorage()) { - payload = tryParse(storage.getDataFromLocalStorage(key, null)) - } - if (payload !== undefined) { - if (payload.expire_at > (Date.now() / 1000)) { - return payload - } - } - } catch (error) { - logError(error); - } -} - -/** - * Store GrowthCode data in either cookie or local storage - * expiration date: 45 days - * @param key - * @param {string} value - */ -function storeData(key, value) { - try { - logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); - if (value) { - if (storage.hasLocalStorage(null)) { - storage.setDataInLocalStorage(key, value, null); - } - } - } catch (error) { - logError(error); - } -} +const MODULE_NAME = 'growthCodeId'; +const GCID_KEY = 'gcid'; -/** - * Parse json if possible, else return null - * @param data - * @param {object|null} - */ -function tryParse(data) { - let payload; - try { - payload = JSON.parse(data); - if (payload == null) { - return undefined - } - return payload - } catch (err) { - return undefined; - } -} +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); /** @type {Submodule} */ export const growthCodeIdSubmodule = { @@ -96,96 +36,40 @@ export const growthCodeIdSubmodule = { decode(value) { return value && value !== '' ? { 'growthCodeId': value } : undefined; }, + /** * performs action to obtain id and return a value in the callback's response argument * @function * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(config, consentData) { + getId(config) { const configParams = (config && config.params) || {}; - if (!configParams || typeof configParams.pid !== 'string') { - logError('User ID - GrowthCodeID submodule requires a valid Partner ID to be defined'); - return; - } - const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const consentString = gdpr ? consentData.consentString : ''; - if (gdpr && !consentString) { - logInfo('Consent string is required to call GrowthCode id.'); - return; - } + let ids = []; + let gcid = storage.getDataFromLocalStorage(GCID_KEY, null) - let publisherId = configParams.publisher_id ? configParams.publisher_id : '_sharedID'; + if (gcid !== null) { + const gcEid = { + source: 'growthcode.io', + uids: [{ + id: gcid, + atype: 3, + }] + } - let sharedId; - if (configParams.publisher_id_storage === 'html5') { - sharedId = storage.getDataFromLocalStorage(publisherId, null) ? (storage.getDataFromLocalStorage(publisherId, null)) : null; - } else { - sharedId = storage.getCookie(publisherId, null) ? (storage.getCookie(publisherId, null)) : null; - } - if (!sharedId) { - logError('User ID - Publisher ID is not correctly setup.'); + ids = ids.concat(gcEid) } - const resp = function(callback) { - let gcData = readData(GC_DATA_KEY); - if (gcData) { - callback(gcData); - } else { - let segment = window.location.pathname.substr(1).replace(/\/+$/, ''); - if (segment === '') { - segment = 'home'; - } - - let url = configParams.url ? configParams.url : ENDPOINT_URL; - url = tryAppendQueryString(url, 'pid', configParams.pid); - url = tryAppendQueryString(url, 'uid', sharedId); - url = tryAppendQueryString(url, 'u', window.location.href); - url = tryAppendQueryString(url, 'h', window.location.hostname); - url = tryAppendQueryString(url, 's', segment); - url = tryAppendQueryString(url, 'r', document.referrer); + let additionalEids = storage.getDataFromLocalStorage(configParams.customerEids, null) + if (additionalEids !== null) { + let data = JSON.parse(additionalEids) + ids = ids.concat(data) + } - ajax(url, { - success: response => { - let respJson = tryParse(response); - // If response is a valid json and should save is true - if (respJson) { - storeData(GC_DATA_KEY, JSON.stringify(respJson)) - storeData(GCID_KEY, respJson.gc_id); - callback(respJson); - } else { - callback(); - } - }, - error: error => { - logError(MODULE_NAME + ': ID fetch encountered an error', error); - callback(); - } - }, undefined, {method: 'GET', withCredentials: true}) - } - }; - return { callback: resp }; + return {id: ids} }, - eids: { - 'growthCodeId': { - getValue: function(data) { - return data.gc_id - }, - source: 'growthcode.io', - atype: 1, - getUidExt: function(data) { - const extendedData = pick(data, [ - 'h1', - 'h2', - 'h3', - ]); - if (Object.keys(extendedData).length) { - return extendedData; - } - } - }, - } + }; submodule('userId', growthCodeIdSubmodule); diff --git a/modules/growthCodeIdSystem.md b/modules/growthCodeIdSystem.md index f804686a7a9..de5344e966b 100644 --- a/modules/growthCodeIdSystem.md +++ b/modules/growthCodeIdSystem.md @@ -18,20 +18,38 @@ pbjs.setConfig({ userIds: [{ name: 'growthCodeId', params: { - pid: 'TEST01', // Set your Partner ID here for production (obtained from Growthcode) - publisher_id: '_sharedID', - publisher_id_storage: 'html5' + customerEids: 'customerEids', } }] } }); ``` -| Param under userSync.userIds[] | Scope | Type | Description | Example | -|--------------------------------|----------|--------| --- |-----------------| -| name | Required | String | The name of this module. | `"growthCodeId"` | -| params | Required | Object | Details of module params. | | -| params.pid | Required | String | This is the Parter ID value obtained from GrowthCode | `"TEST01"` | -| params.url | Optional | String | Custom URL for server | | -| params.publisher_id | Optional | String | Name if the variable that holds your publisher ID | `"_sharedID"` | -| params.publisher_id_storage | Optional | String | Publisher ID storage (cookie, html5) | `"html5"` | +### Sample Eids +Below is an example of the EIDs stored in Local Store (customerEids) +```json +[ + { + "source":"domain.com", + "uids":[ + { + "id":"8212212191539393121", + "ext":{ + "stype":"ppuid" + } + } + ] + }, + { + "source":"example.com", + "uids":[ + { + "id":"e06e9e5a-273c-46f8-aace-6f62cf13ea71", + "ext":{ + "stype":"ppuid" + } + } + ] + } +] +``` diff --git a/modules/growthCodeRtdProvider.js b/modules/growthCodeRtdProvider.js index 370ace9a203..b12b25a0951 100644 --- a/modules/growthCodeRtdProvider.js +++ b/modules/growthCodeRtdProvider.js @@ -5,10 +5,11 @@ import { submodule } from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; import { - logMessage, logError, tryAppendQueryString, mergeDeep + logMessage, logError, mergeDeep } from '../src/utils.js'; import * as ajax from '../src/ajax.js'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const MODULE_NAME = 'growthCodeRtd'; const LOG_PREFIX = 'GrowthCodeRtd: '; @@ -59,7 +60,11 @@ function init(config, userConsent) { items = tryParse(storage.getDataFromLocalStorage(RTD_CACHE_KEY, null)); - return callServer(configParams, items, expiresAt, userConsent); + if (configParams.pid === undefined) { + return true; // Die gracefully + } else { + return callServer(configParams, items, expiresAt, userConsent); + } } function callServer(configParams, items, expiresAt, userConsent) { // Expire Cache @@ -76,8 +81,8 @@ function callServer(configParams, items, expiresAt, userConsent) { url = tryAppendQueryString(url, 'pid', configParams.pid); url = tryAppendQueryString(url, 'u', window.location.href); url = tryAppendQueryString(url, 'gcid', gcid); - if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentData.getTCData.tcString)) { - url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentData.getTCData.tcString) + if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentString)) { + url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentString) } ajax.ajaxBuilder()(url, { diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index d050af4ac8f..ff249ec9b50 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -6,6 +6,15 @@ import {getStorageManager} from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'gumgum'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); const ALIAS_BIDDER_CODE = ['gg']; @@ -14,6 +23,7 @@ const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const TIME_TO_LIVE = 60; const DELAY_REQUEST_TIME = 1800000; // setting to 30 mins +const pubProvidedIdSources = ['dac.co.jp', 'audigent.com', 'id5-sync.com', 'liveramp.com', 'intentiq.com', 'liveintent.com', 'crwdcntrl.net', 'quantcast.com', 'adserver.org', 'yahoo.com'] let invalidRequestIds = {}; let pageViewId = null; @@ -175,6 +185,7 @@ function _getVidParams(attributes) { linearity: li, startdelay: sd, placement: pt, + plcmt, protocols = [], playerSize = [] } = attributes; @@ -186,7 +197,7 @@ function _getVidParams(attributes) { pr = protocols.join(','); } - return { + const result = { mind, maxd, li, @@ -196,6 +207,11 @@ function _getVidParams(attributes) { viw, vih }; + // Add vplcmt property to the result object if plcmt is available + if (plcmt !== undefined && plcmt !== null) { + result.vplcmt = plcmt; + } + return result; } /** @@ -302,7 +318,8 @@ function buildRequests(validBidRequests, bidderRequest) { const gpid = deepAccess(ortb2Imp, 'ext.data.pbadslot') || deepAccess(ortb2Imp, 'ext.data.adserver.adslot'); let sizes = [1, 1]; let data = {}; - + data.displaymanager = 'Prebid.js - gumgum'; + data.displaymanagerver = '$prebid.version$'; const date = new Date(); const lt = date.getTime(); const to = date.getTimezoneOffset(); @@ -310,7 +327,23 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-174 Removed unnecessary checks to fix failing test data.lt = lt; data.to = to; - + function jsoStringifynWithMaxLength(data, maxLength) { + let jsonString = JSON.stringify(data); + if (jsonString.length <= maxLength) { + return jsonString; + } else { + const truncatedData = data.slice(0, Math.floor(data.length * (maxLength / jsonString.length))); + jsonString = JSON.stringify(truncatedData); + return jsonString; + } + } + // Send filtered pubProvidedId's + if (userId && userId.pubProvidedId) { + let filteredData = userId.pubProvidedId.filter(item => pubProvidedIdSources.includes(item.source)); + let maxLength = 1800; // replace this with your desired maximum length + let truncatedJsonString = jsoStringifynWithMaxLength(filteredData, maxLength); + data.pubProvidedId = truncatedJsonString + } // ADJS-1286 Read id5 id linktype field if (userId && userId.id5id && userId.id5id.uid && userId.id5id.ext) { data.id5Id = userId.id5id.uid || null @@ -322,9 +355,6 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-134 Retrieve ID envelopes for (const eid in eids) data[eid] = eids[eid]; - // ADJS-1024 & ADSS-1297 & ADTS-175 - gpid && (data.gpid = gpid); - if (mediaTypes.banner) { sizes = mediaTypes.banner.sizes; } else if (mediaTypes.video) { @@ -332,6 +362,9 @@ function buildRequests(validBidRequests, bidderRequest) { data = _getVidParams(mediaTypes.video); } + // ADJS-1024 & ADSS-1297 & ADTS-175 + gpid && (data.gpid = gpid); + if (pageViewId) { data.pv = pageViewId; } @@ -383,15 +416,15 @@ function buildRequests(validBidRequests, bidderRequest) { data.uspConsent = uspConsent; } if (gppConsent) { - data.gppConsent = { - gppString: bidderRequest.gppConsent.gppString, - gpp_sid: bidderRequest.gppConsent.applicableSections - } + data.gppString = bidderRequest.gppConsent.gppString ? bidderRequest.gppConsent.gppString : '' + data.gppSid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' } else if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) { - data.gppConsent = { - gppString: bidderRequest.ortb2.regs.gpp, - gpp_sid: bidderRequest.ortb2.regs.gpp_sid - }; + data.gppString = bidderRequest.ortb2.regs.gpp + data.gppSid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid.join(',') : '' + } + const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa'); + if (dsa) { + data.dsa = dsa } if (coppa) { data.coppa = coppa; @@ -520,15 +553,15 @@ function interpretResponse(serverResponse, bidRequest) { mediaType: type || mediaType }; let sizes = parseSizesInput(bidRequest.sizes); - if (maxw && maxh) { sizes = [`${maxw}x${maxh}`]; } else if (product === 5 && includes(sizes, '1x1')) { sizes = ['1x1']; - } else if (product === 2 && includes(sizes, '1x1')) { + // added logic for in-slot multi-szie + } else if ((product === 2 && includes(sizes, '1x1')) || product === 3) { const requestSizesThatMatchResponse = (bidRequest.sizes && bidRequest.sizes.reduce((result, current) => { const [ width, height ] = current; - if (responseWidth === width || responseHeight === height) result.push(current.join('x')); + if (responseWidth === width && responseHeight === height) result.push(current.join('x')); return result }, [])) || []; sizes = requestSizesThatMatchResponse.length ? requestSizesThatMatchResponse : parseSizesInput(bidRequest.sizes) diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index c60f0f812a4..66cb5624a38 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -9,14 +9,23 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isPlainObject, logError, logInfo} from '../src/utils.js'; +import { config } from '../src/config.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const LOG_PREFIX = '[hadronIdSystem]'; const HADRONID_LOCAL_NAME = 'auHadronId'; const MODULE_NAME = 'hadronId'; const AU_GVLID = 561; const DEFAULT_HADRON_URL_ENDPOINT = 'https://id.hadron.ad.gt/api/v1/pbhid'; -export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'hadron'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** * Param or default. @@ -42,6 +51,8 @@ const urlAddParams = (url, params) => { return url + (url.indexOf('?') > -1 ? '&' : '?') + params } +const isDebug = config.getConfig('debug') || false; + /** @type {Submodule} */ export const hadronIdSubmodule = { /** @@ -88,7 +99,7 @@ export const hadronIdSubmodule = { } catch (error) { logError(error); } - logInfo(`Response from backend is ${responseObj}`); + logInfo(LOG_PREFIX, `Response from backend is ${response}`, responseObj); hadronId = responseObj['hadronId']; storage.setDataInLocalStorage(HADRONID_LOCAL_NAME, hadronId); responseObj = {id: {hadronId}}; @@ -100,13 +111,34 @@ export const hadronIdSubmodule = { callback(); } }; - logInfo('HadronId not found in storage, calling backend...'); - const url = urlAddParams( + let url = urlAddParams( // config.params.url and config.params.urlArg are not documented // since their use is for debugging purposes only paramOrDefault(config.params.url, DEFAULT_HADRON_URL_ENDPOINT, config.params.urlArg), - `partner_id=${partnerId}&_it=prebid` + `partner_id=${partnerId}&_it=prebid&t=1&src=id` // src=id => the backend was called from getId ); + if (isDebug) { + url += '&debug=1' + } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent) { + url += `${gdprConsent.consentString ? '&gdprString=' + encodeURIComponent(gdprConsent.consentString) : ''}`; + url += `&gdpr=${gdprConsent.gdprApplies === true ? 1 : 0}`; + } + + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString) { + url += `&us_privacy=${encodeURIComponent(usPrivacyString)}`; + } + + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + url += `${gppConsent.gppString ? '&gpp=' + encodeURIComponent(gppConsent.gppString) : ''}`; + url += `${gppConsent.applicableSections ? '&gpp_sid=' + encodeURIComponent(gppConsent.applicableSections) : ''}`; + } + + logInfo(LOG_PREFIX, `hadronId not found in storage, calling home (${url})`); + ajax(url, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; diff --git a/modules/hadronIdSystem.md b/modules/hadronIdSystem.md index 7521cca06ac..212030cbcd9 100644 --- a/modules/hadronIdSystem.md +++ b/modules/hadronIdSystem.md @@ -35,5 +35,4 @@ The below parameters apply only to the HadronID User ID Module integration. | value | Optional | Object | Used only if the page has a separate mechanism for storing the Hadron ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"hadronId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | | params | Optional | Object | Used to store params for the id system | | params.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | -| params.url | Optional | String | Set an alternate GET url for HadronId with this parameter | -| params.urlArg | Optional | Object | Optional url parameter for params.url | + | diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index 6fb982815c1..5c604709b4b 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -14,6 +14,10 @@ import {isFn, isStr, isArray, deepEqual, isPlainObject, logError, logInfo} from import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const LOG_PREFIX = 'User ID - HadronRtdProvider submodule: '; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'hadron'; diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js index 2073063168d..fbcbb9492c7 100644 --- a/modules/holidBidAdapter.js +++ b/modules/holidBidAdapter.js @@ -1,7 +1,6 @@ import { deepAccess, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, isStr, logMessage, triggerPixel, diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js index f746e69cbba..01d29ee0126 100644 --- a/modules/hybridBidAdapter.js +++ b/modules/hybridBidAdapter.js @@ -1,10 +1,16 @@ import {_map, deepAccess, isArray, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {auctionManager} from '../src/auctionManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'hybrid'; const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; const TRAFFIC_TYPE_WEB = 1; @@ -88,16 +94,13 @@ function buildBid(bidData) { bid.vastXml = bidData.content; bid.mediaType = VIDEO; - // TODO: why does this need to iterate through every ad unit? - let adUnit = find(auctionManager.getAdUnits(), function (unit) { - return unit.transactionId === bidData.transactionId; - }); + const video = bidData.mediaTypes?.video; - if (adUnit) { - bid.width = adUnit.mediaTypes.video.playerSize[0][0]; - bid.height = adUnit.mediaTypes.video.playerSize[0][1]; + if (video) { + bid.width = video.playerSize[0][0]; + bid.height = video.playerSize[0][1]; - if (adUnit.mediaTypes.video.context === 'outstream') { + if (video.context === 'outstream') { bid.renderer = createRenderer(bid); } } diff --git a/modules/hypelabBidAdapter.js b/modules/hypelabBidAdapter.js index a625c7299a6..14a4758bd27 100644 --- a/modules/hypelabBidAdapter.js +++ b/modules/hypelabBidAdapter.js @@ -1,6 +1,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { generateUUID } from '../src/utils.js'; +import { generateUUID, isFn, isPlainObject } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; export const BIDDER_CODE = 'hypelab'; @@ -12,7 +12,7 @@ export const REPORTING_ROUTE = ''; const PREBID_VERSION = '$prebid.version$'; const PROVIDER_NAME = 'prebid'; -const PROVIDER_VERSION = '0.0.1'; +const PROVIDER_VERSION = '0.0.2'; const url = (route) => ENDPOINT_URL + route; @@ -38,18 +38,24 @@ function buildRequests(validBidRequests, bidderRequest) { const uuid = uids[0] ? uids[0] : generateTemporaryUUID(); + const floor = getBidFloor(request, request.sizes || []); + + const dpr = typeof window != 'undefined' ? window.devicePixelRatio : 1; + const payload = { property_slug: request.params.property_slug, placement_slug: request.params.placement_slug, provider_version: PROVIDER_VERSION, provider_name: PROVIDER_NAME, - referrer: + location: bidderRequest.refererInfo?.page || typeof window != 'undefined' ? window.location.href : '', sdk_version: PREBID_VERSION, sizes: request.sizes, wids: [], + floor, + dpr, uuid, bidRequestsCount: request.bidRequestsCount, bidderRequestsCount: request.bidderRequestsCount, @@ -79,6 +85,26 @@ function generateTemporaryUUID() { return 'tmp_' + generateUUID(); } +function getBidFloor(bid, sizes) { + if (!isFn(bid.getFloor)) { + return bid.params.bidFloor ? bid.params.bidFloor : null; + } + + let floor; + + let floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: sizes.length === 1 ? sizes[0] : '*' + }); + + if (isPlainObject(floorInfo) && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + + return floor; +} + function interpretResponse(serverResponse, bidRequest) { const { data } = serverResponse.body; @@ -94,12 +120,12 @@ function interpretResponse(serverResponse, bidRequest) { creativeId: data.creative_set_slug, currency: data.currency, netRevenue: true, - referrer: bidRequest.data.referrer, + referrer: bidRequest.data.location, ttl: data.ttl, ad: data.html, mediaType: serverResponse.body.data.media_type, meta: { - advertiserDomains: data.advertiserDomains || [], + advertiserDomains: data.advertiser_domains || [], }, }; diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index 994be7c0804..b9de7ef4e46 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -1,7 +1,9 @@ -import { submodule } from '../src/hook.js'; +import {submodule} from '../src/hook.js'; import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -76,7 +78,7 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { if (!utils.isEmpty(adSlot)) { p = adSlot.gptSlot; } else { - if (!utils.isEmpty(adUnitPath) && utils.hasOwn(adUnitPath, bidRequest.code)) { + if (!utils.isEmpty(adUnitPath) && adUnitPath.hasOwnProperty(bidRequest.code)) { if (utils.isStr(adUnitPath[bidRequest.code]) && !utils.isEmpty(adUnitPath[bidRequest.code])) { p = adUnitPath[bidRequest.code]; } @@ -86,13 +88,13 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { } function stringifySlot(bidRequest, adUnitPath) { - const sizes = utils.getAdUnitSizes(bidRequest); + const sizes = getAdUnitSizes(bidRequest); const id = bidRequest.code; const ss = stringifySlotSizes(sizes); - const adSlot = utils.getGptSlotInfoForAdUnitCode(bidRequest.code); + const adSlot = getGptSlotInfoForAdUnitCode(bidRequest.code); const p = getAdUnitPath(adSlot, bidRequest, adUnitPath); const slot = { id, ss, p }; - const keyValues = utils.getKeys(slot).map(function (key) { + const keyValues = Object.keys(slot).map(function (key) { return [key, slot[key]].join(':'); }); return '{' + keyValues.join(',') + '}'; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index eff1f136649..42f0044edc3 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -10,17 +10,27 @@ import { deepSetValue, isEmpty, isEmptyStr, + isPlainObject, logError, logInfo, logWarn, safeJSONParse } from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; +import {fetch} 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'; +import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {GreedyPromise} from '../src/utils/promise.js'; +import {loadExternalScript} from '../src/adloader.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = 'id5Id'; const GVLID = 131; @@ -30,6 +40,7 @@ export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`; const LOCAL_STORAGE = 'html5'; const LOG_PREFIX = 'User ID - ID5 submodule: '; const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid'; +const ID5_DOMAIN = 'id5-sync.com'; // order the legacy cookie names in reverse priority order so the last // cookie in the array is the most preferred to use @@ -37,6 +48,70 @@ const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +/** + * @typedef {Object} IdResponse + * @property {string} [universal_uid] - The encrypted ID5 ID to pass to bidders + * @property {Object} [ext] - The extensions object to pass to bidders + * @property {Object} [ab_testing] - A/B testing configuration + */ + +/** + * @typedef {Object} FetchCallConfig + * @property {string} [url] - The URL for the fetch endpoint + * @property {Object} [overrides] - Overrides to apply to fetch parameters + */ + +/** + * @typedef {Object} ExtensionsCallConfig + * @property {string} [url] - The URL for the extensions endpoint + * @property {string} [method] - Overrides the HTTP method to use to make the call + * @property {Object} [body] - Specifies a body to pass to the extensions endpoint + */ + +/** + * @typedef {Object} DynamicConfig + * @property {FetchCallConfig} [fetchCall] - The fetch call configuration + * @property {ExtensionsCallConfig} [extensionsCall] - The extensions call configuration + */ + +/** + * @typedef {Object} ABTestingConfig + * @property {boolean} enabled - Tells whether A/B testing is enabled for this instance + * @property {number} controlGroupPct - A/B testing probability + */ + +/** + * @typedef {Object} Multiplexing + * @property {boolean} [disabled] - Disable multiplexing (instance will work in single mode) + */ + +/** + * @typedef {Object} Diagnostics + * @property {boolean} [publishingDisabled] - Disable diagnostics publishing + * @property {number} [publishAfterLoadInMsec] - Delay in ms after script load after which collected diagnostics are published + * @property {boolean} [publishBeforeWindowUnload] - When true, diagnostics publishing is triggered on Window 'beforeunload' event + * @property {number} [publishingSampleRatio] - Diagnostics publishing sample ratio + */ + +/** + * @typedef {Object} Segment + * @property {string} [destination] - GVL ID or ID5-XX Partner ID. Mandatory + * @property {Array} [ids] - The segment IDs to push. Must contain at least one segment ID. + */ + +/** + * @typedef {Object} Id5PrebidConfig + * @property {number} partner - The ID5 partner ID + * @property {string} pd - The ID5 partner data string + * @property {ABTestingConfig} abTesting - The A/B testing configuration + * @property {boolean} disableExtensions - Disabled extensions call + * @property {string} [externalModuleUrl] - URL for the id5 prebid external module + * @property {Multiplexing} [multiplexing] - Multiplexing options. Only supported when loading the external module. + * @property {Diagnostics} [diagnostics] - Diagnostics options. Supported only in multiplexing + * @property {Array} [segments] - A list of segments to push to partners. Supported only in multiplexing. + * @property {boolean} [disableUaHints] - When true, look up of high entropy values through user agent hints is disabled. + */ + /** @type {Submodule} */ export const id5IdSubmodule = { /** @@ -73,9 +148,17 @@ export const id5IdSubmodule = { id5id: { uid: universalUid, ext: ext - } + }, }; + if (isPlainObject(ext.euid)) { + responseObj.euid = { + uid: ext.euid.uids[0].id, + source: ext.euid.source, + ext: {provider: ID5_DOMAIN} + } + } + const abTestingResult = deepAccess(value, 'ab_testing.result'); switch (abTestingResult) { case 'control': @@ -108,7 +191,7 @@ export const id5IdSubmodule = { * @returns {IdResponse|undefined} */ getId(submoduleConfig, consentData, cacheIdObj) { - if (!hasRequiredConfig(submoduleConfig)) { + if (!validateConfig(submoduleConfig)) { return undefined; } @@ -118,7 +201,8 @@ export const id5IdSubmodule = { } const resp = function (cbFunction) { - new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute() + const fetchFlow = new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData(), gppDataHandler.getConsentData()); + fetchFlow.execute() .then(response => { cbFunction(response) }) @@ -142,14 +226,12 @@ export const id5IdSubmodule = { * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ extendId(config, consentData, cacheIdObj) { - hasRequiredConfig(config); - if (!hasWriteConsentToLocalStorage(consentData)) { logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.') return cacheIdObj; } - const partnerId = (config && config.params && config.params.partner) || 0; + const partnerId = validateConfig(config) ? config.params.partner : 0; incrementNb(partnerId); logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj); @@ -160,7 +242,7 @@ export const id5IdSubmodule = { getValue: function(data) { return data.uid }, - source: 'id5-sync.com', + source: ID5_DOMAIN, atype: 1, getUidExt: function(data) { if (data.ext) { @@ -168,94 +250,123 @@ export const id5IdSubmodule = { } } }, + 'euid': { + getValue: function (data) { + return data.uid; + }, + getSource: function (data) { + return data.source; + }, + atype: 3, + getUidExt: function (data) { + if (data.ext) { + return data.ext; + } + } + } }, }; -class IdFetchFlow { - constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData) { +export class IdFetchFlow { + constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) { this.submoduleConfig = submoduleConfig this.gdprConsentData = gdprConsentData this.cacheIdObj = cacheIdObj this.usPrivacyData = usPrivacyData + this.gppData = gppData } - execute() { - return this.#callForConfig(this.submoduleConfig) - .then(fetchFlowConfig => { - return this.#callForExtensions(fetchFlowConfig.extensionsCall) - .then(extensionsData => { - return this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData) - }) - }) - .then(fetchCallResponse => { - try { - resetNb(this.submoduleConfig.params.partner); - if (fetchCallResponse.privacy) { - storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); - } - } catch (error) { - logError(LOG_PREFIX + error); - } - return fetchCallResponse; - }) + /** + * Calls the ID5 Servers to fetch an ID5 ID + * @returns {Promise} The result of calling the server side + */ + async execute() { + const configCallPromise = this.#callForConfig(); + if (this.#isExternalModule()) { + try { + return await this.#externalModuleFlow(configCallPromise); + } catch (error) { + logError(LOG_PREFIX + 'Error while performing ID5 external module flow. Continuing with regular flow.', error); + return this.#regularFlow(configCallPromise); + } + } else { + return this.#regularFlow(configCallPromise); + } } - #ajaxPromise(url, data, options) { - return new Promise((resolve, reject) => { - ajax(url, - { - success: function (res) { - resolve(res) - }, - error: function (err) { - reject(err) - } - }, data, options) - }) + #isExternalModule() { + return typeof this.submoduleConfig.params.externalModuleUrl === 'string'; } // eslint-disable-next-line no-dupe-class-members - #callForConfig(submoduleConfig) { - let url = submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only - return this.#ajaxPromise(url, JSON.stringify(submoduleConfig), {method: 'POST'}) - .then(response => { - let responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'config response received from the server', responseObj); - return responseObj; - }); + async #externalModuleFlow(configCallPromise) { + await loadExternalModule(this.submoduleConfig.params.externalModuleUrl); + const fetchFlowConfig = await configCallPromise; + + return this.#getExternalIntegration().fetchId5Id(fetchFlowConfig, this.submoduleConfig.params, getRefererInfo(), this.gdprConsentData, this.usPrivacyData, this.gppData); } // eslint-disable-next-line no-dupe-class-members - #callForExtensions(extensionsCallConfig) { + #getExternalIntegration() { + return window.id5Prebid && window.id5Prebid.integration; + } + + // eslint-disable-next-line no-dupe-class-members + async #regularFlow(configCallPromise) { + const fetchFlowConfig = await configCallPromise; + const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall); + const fetchCallResponse = await this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData); + return this.#processFetchCallResponse(fetchCallResponse); + } + + // eslint-disable-next-line no-dupe-class-members + async #callForConfig() { + let url = this.submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(this.submoduleConfig) + }); + if (!response.ok) { + throw new Error('Error while calling config endpoint: ', response); + } + const dynamicConfig = await response.json(); + logInfo(LOG_PREFIX + 'config response received from the server', dynamicConfig); + return dynamicConfig; + } + + // eslint-disable-next-line no-dupe-class-members + async #callForExtensions(extensionsCallConfig) { if (extensionsCallConfig === undefined) { - return Promise.resolve(undefined) + return undefined; + } + const extensionsUrl = extensionsCallConfig.url; + const method = extensionsCallConfig.method || 'GET'; + const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}); + const response = await fetch(extensionsUrl, { method, body }); + if (!response.ok) { + throw new Error('Error while calling extensions endpoint: ', response); } - let extensionsUrl = extensionsCallConfig.url - let method = extensionsCallConfig.method || 'GET' - let data = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}) - return this.#ajaxPromise(extensionsUrl, data, {'method': method}) - .then(response => { - let responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'extensions response received from the server', responseObj); - return responseObj; - }) + const extensions = await response.json(); + logInfo(LOG_PREFIX + 'extensions response received from the server', extensions); + return extensions; } // eslint-disable-next-line no-dupe-class-members - #callId5Fetch(fetchCallConfig, extensionsData) { - let url = fetchCallConfig.url; - let additionalData = fetchCallConfig.overrides || {}; - let data = { + async #callId5Fetch(fetchCallConfig, extensionsData) { + const fetchUrl = fetchCallConfig.url; + const additionalData = fetchCallConfig.overrides || {}; + const body = JSON.stringify({ ...this.#createFetchRequestData(), ...additionalData, extensions: extensionsData - }; - return this.#ajaxPromise(url, JSON.stringify(data), {method: 'POST', withCredentials: true}) - .then(response => { - let responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'fetch response received from the server', responseObj); - return responseObj; - }); + }); + const response = await fetch(fetchUrl, { method: 'POST', body, credentials: 'include' }); + if (!response.ok) { + throw new Error('Error while calling fetch endpoint: ', response); + } + const fetchResponse = await response.json(); + logInfo(LOG_PREFIX + 'fetch response received from the server', fetchResponse); + return fetchResponse; } // eslint-disable-next-line no-dupe-class-members @@ -264,7 +375,7 @@ class IdFetchFlow { const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0; const referer = getRefererInfo(); const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature(); - const nbPage = incrementNb(params.partner); + const nbPage = incrementAndResetNb(params.partner); const data = { 'partner': params.partner, 'gdpr': hasGdpr, @@ -287,6 +398,11 @@ class IdFetchFlow { if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) { data.us_privacy = this.usPrivacyData; } + if (this.gppData) { + data.gpp_string = this.gppData.gppString; + data.gpp_sid = this.gppData.applicableSections; + } + if (signature !== undefined && !isEmptyStr(signature)) { data.s = signature; } @@ -305,11 +421,52 @@ class IdFetchFlow { } return data; } + + // eslint-disable-next-line no-dupe-class-members + #processFetchCallResponse(fetchCallResponse) { + try { + if (fetchCallResponse.privacy) { + storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); + } + } catch (error) { + logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error); + } + return fetchCallResponse; + } +} + +async function loadExternalModule(url) { + return new GreedyPromise((resolve, reject) => { + if (window.id5Prebid) { + // Already loaded + resolve(); + } else { + try { + loadExternalScript(url, 'id5', resolve); + } catch (error) { + reject(error); + } + } + }); } -function hasRequiredConfig(config) { - if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { - logError(LOG_PREFIX + 'partner required to be defined as a number'); +function validateConfig(config) { + if (!config || !config.params || !config.params.partner) { + logError(LOG_PREFIX + 'partner required to be defined'); + return false; + } + + const partner = config.params.partner + if (typeof partner === 'string' || partner instanceof String) { + let parsedPartnerId = parseInt(partner); + if (isNaN(parsedPartnerId) || parsedPartnerId < 0) { + logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer'); + return false; + } else { + config.params.partner = parsedPartnerId; + } + } else if (typeof partner !== 'number') { + logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer'); return false; } @@ -353,8 +510,10 @@ function incrementNb(partnerId) { return nb; } -function resetNb(partnerId) { +function incrementAndResetNb(partnerId) { + const result = incrementNb(partnerId); storeNbInCache(partnerId, 0); + return result; } function getLegacyCookieSignature() { @@ -393,7 +552,7 @@ export function getFromLocalStorage(key) { * by default it's not required * @param {string} key * @param {any} value - * @param {integer} expDays + * @param {number} expDays */ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index cf90290b1d8..592c69056fa 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,6 +1,6 @@ -# ID5 Universal ID +# ID5 ID -The ID5 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 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://support.id5.io/portal/en/kb/articles/prebid-js-user-id-module). +The ID5 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 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/prebid-user-id-module/id5-prebid-user-id-module). ## ID5 ID Registration @@ -25,11 +25,12 @@ pbjs.setConfig({ name: 'id5Id', params: { partner: 173, // change to the Partner Number you received from ID5 + externalModuleUrl: "https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js" // optional but recommended 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) - }, + }, disableExtensions: false // optional }, storage: { @@ -49,7 +50,8 @@ pbjs.setConfig({ | name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | | params | Required | Object | Details for the ID5 ID. | | | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.externalModuleUrl | Optional | String | The URL for the id5-prebid external module. It is recommended to use the latest version at the URL in the example. Source code available [here](https://github.com/id5io/id5-api.js/blob/master/src/id5PrebidModule.js). | https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) 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` | @@ -68,3 +70,6 @@ pbjs.setConfig({ Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of users you would like to set for the control group. The control group is the set of user where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the user was in the control group or not. It's important to note that the control group is user based, and not request based. In other words, from one page view to another, a user will always be in or out of the control group. + +### A Note on Using Multiple Wrappers +If you or your monetization partners are deploying multiple Prebid wrappers on your websites, you should make sure you add the ID5 ID User ID module to *every* wrapper. Only the bidders configured in the Prebid wrapper where the ID5 ID User ID module is installed and configured will be able to pick up the ID5 ID. Bidders from other Prebid instances will not be able to pick up the ID5 ID. diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js index 29dda216fdc..dd08a132b2d 100644 --- a/modules/idWardRtdProvider.js +++ b/modules/idWardRtdProvider.js @@ -9,6 +9,9 @@ import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'idWard'; @@ -28,9 +31,9 @@ function addRealTimeData(ortb2, rtd) { } /** - * Try parsing stringified array of segment IDs. - * @param {String} data - */ + * Try parsing stringified array of segment IDs. + * @param {String} data + */ function tryParse(data) { try { return JSON.parse(data); @@ -41,12 +44,12 @@ function tryParse(data) { } /** - * Real-time data retrieval from ID Ward - * @param {Object} reqBidsConfigObj - * @param {function} onDone - * @param {Object} rtdConfig - * @param {Object} userConsent - */ + * Real-time data retrieval from ID Ward + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + */ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent) { if (rtdConfig && isPlainObject(rtdConfig.params)) { const jsonData = storage.getDataFromLocalStorage(rtdConfig.params.cohortStorageKey) @@ -85,11 +88,11 @@ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent } /** - * Module init - * @param {Object} provider - * @param {Object} userConsent - * @return {boolean} - */ + * Module init + * @param {Object} provider + * @param {Object} userConsent + * @return {boolean} + */ function init(provider, userConsent) { return true; } diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index e8cef34f41e..82aa2303e1c 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -10,11 +10,21 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { gppDataHandler } from '../src/adapterManager.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = 'identityLink'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +const liverampEnvelopeName = '_lr_env'; + /** @type {Submodule} */ export const identityLinkSubmodule = { /** @@ -51,18 +61,21 @@ export const identityLinkSubmodule = { } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const gdprConsentString = hasGdpr ? consentData.consentString : ''; - const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { utils.logInfo('identityLink: Consent string is required to call envelope API.'); return; } - const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; + const gppData = gppDataHandler.getConsentData(); + const gppString = gppData && gppData.gppString ? gppData.gppString : false; + const gppSectionId = gppData && gppData.gppString && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== -1 ? gppData.applicableSections[0] : false; + const hasGpp = gppString && gppSectionId; + const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=4&cv=' + gdprConsentString : ''}${hasGpp ? '&gpp=' + gppString + '&gpp_sid=' + gppSectionId : ''}`; let resp; resp = function (callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint - if (window.ats) { + if (window.ats && window.ats.retrieveEnvelope) { utils.logInfo('identityLink: ATS exists!'); window.ats.retrieveEnvelope(function (envelope) { if (envelope) { @@ -74,7 +87,14 @@ export const identityLinkSubmodule = { } }); } else { - getEnvelope(url, callback, configParams); + // try to get envelope directly from storage if ats lib is not present on a page + let envelope = getEnvelopeFromStorage(); + if (envelope) { + utils.logInfo('identityLink: LiveRamp envelope successfully retrieved from storage!'); + callback(JSON.parse(envelope).envelope); + } else { + getEnvelope(url, callback, configParams); + } } }; @@ -127,4 +147,9 @@ function setEnvelopeSource(src) { storage.setCookie('_lr_env_src_ats', src, now.toUTCString()); } +export function getEnvelopeFromStorage() { + let rawEnvelope = storage.getCookie(liverampEnvelopeName) || storage.getDataFromLocalStorage(liverampEnvelopeName); + return rawEnvelope ? window.atob(rawEnvelope) : undefined; +} + submodule('userId', identityLinkSubmodule); diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js index bf807f199a6..db545eecd8c 100644 --- a/modules/idxIdSystem.js +++ b/modules/idxIdSystem.js @@ -9,6 +9,11 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const IDX_MODULE_NAME = 'idx'; const IDX_COOKIE_NAME = '_idx'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: IDX_MODULE_NAME}); diff --git a/modules/illuminBidAdapter.js b/modules/illuminBidAdapter.js new file mode 100644 index 00000000000..45b74bec5c9 --- /dev/null +++ b/modules/illuminBidAdapter.js @@ -0,0 +1,338 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'illumin'; +const BIDDER_VERSION = '1.0.0'; +const GVLID = 149; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.illumin.com`; +} + +export function extractCID(params) { + return params.cId; +} + +export function extractPID(params) { + return params.pId; +} + +export function extractSubDomain(params) { + return params.subDomain; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.illumin.com/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.illumin.com/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + gvlid: GVLID, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/illuminBidAdapter.md b/modules/illuminBidAdapter.md new file mode 100644 index 00000000000..8ca656230e4 --- /dev/null +++ b/modules/illuminBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Illumin Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** integrations@illumin.com + +# Description + +Module that connects to Illumin's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'illumin', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index 26d49c49f8c..78681c2beda 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -20,6 +20,10 @@ import { import {submodule} from '../src/hook.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + export const imUidLocalName = '__im_uid'; export const imVidCookieName = '_im_vid'; export const imRtdLocalName = '__im_sids'; @@ -50,8 +54,8 @@ function getSegments(segments, moduleConfig) { } /** -* @param {string} bidderName -*/ + * @param {string} bidderName + */ export function getBidderFunction(bidderName) { const biddersFunction = { pubmatic: function (bid, data, moduleConfig) { diff --git a/modules/imdsBidAdapter.js b/modules/imdsBidAdapter.js index 545a0bd1ac3..4cad1d614c5 100644 --- a/modules/imdsBidAdapter.js +++ b/modules/imdsBidAdapter.js @@ -1,14 +1,16 @@ 'use strict'; -import {deepAccess, deepSetValue, getAdUnitSizes, isFn, isPlainObject, logWarn} from '../src/utils.js'; +import {deepAccess, deepSetValue, isFn, isPlainObject, logWarn, mergeDeep} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BID_SCHEME = 'https://'; const BID_DOMAIN = 'technoratimedia.com'; -const USER_SYNC_HOST = 'https://ad-cdn.technoratimedia.com'; +const USER_SYNC_IFRAME_URL = 'https://ad-cdn.technoratimedia.com/html/usersync.html'; +const USER_SYNC_PIXEL_URL = 'https://sync.technoratimedia.com/services'; const VIDEO_PARAMS = [ 'minduration', 'maxduration', 'startdelay', 'placement', 'linearity', 'mimes', 'protocols', 'api' ]; const BLOCKED_AD_SIZES = [ '1x1', @@ -38,11 +40,11 @@ export const spec = { return; } const refererInfo = bidderRequest.refererInfo; - const openRtbBidRequest = { + // start with some defaults, overridden by anything set in ortb2, if provided. + const openRtbBidRequest = mergeDeep({ id: bidderRequest.bidderRequestId, site: { - // TODO: does the fallback make sense here? - domain: refererInfo.domain || location.hostname, + domain: refererInfo.domain, page: refererInfo.page, ref: refererInfo.ref }, @@ -50,7 +52,7 @@ export const spec = { ua: navigator.userAgent }, imp: [] - }; + }, bidderRequest.ortb2 || {}); const tmax = bidderRequest.timeout; if (tmax) { @@ -101,9 +103,17 @@ export const spec = { } }); - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openRtbBidRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + // Move us_privacy from regs.ext to regs if there isn't already a us_privacy in regs + if (openRtbBidRequest.regs?.ext?.us_privacy && !openRtbBidRequest.regs?.us_privacy) { + deepSetValue(openRtbBidRequest, 'regs.us_privacy', openRtbBidRequest.regs.ext.us_privacy); + } + + // Remove regs.ext.us_privacy + if (openRtbBidRequest.regs?.ext?.us_privacy) { + delete openRtbBidRequest.regs.ext.us_privacy; + if (Object.keys(openRtbBidRequest.regs.ext).length < 1) { + delete openRtbBidRequest.regs.ext; + } } // User ID @@ -117,7 +127,7 @@ export const spec = { if (openRtbBidRequest.imp.length && seatId) { return { method: 'POST', - url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=$$REPO_AND_VERSION$$`, + url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=pbjs%2F$prebid.version$`, data: openRtbBidRequest, options: { contentType: 'application/json', @@ -214,7 +224,6 @@ export const spec = { }; if (!serverResponse.body || typeof serverResponse.body != 'object') { - logWarn('IMDS: server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); return; } const {id, seatbid: seatbids} = serverResponse.body; @@ -294,16 +303,31 @@ export const spec = { } return bids; }, - getUserSyncs: function (syncOptions, serverResponses) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { const syncs = []; + const queryParams = ['src=pbjs%2F$prebid.version$']; + if (gdprConsent) { + queryParams.push(`gdpr=${Number(gdprConsent.gdprApplies && 1)}&consent=${encodeURIComponent(gdprConsent.consentString || '')}`); + } + if (uspConsent) { + queryParams.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + if (gppConsent) { + queryParams.push('gpp=' + encodeURIComponent(gppConsent.gppString || '') + '&gppsid=' + encodeURIComponent((gppConsent.applicableSections || []).join(','))); + } + if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `${USER_SYNC_HOST}/html/usersync.html?src=$$REPO_AND_VERSION$$` + url: `${USER_SYNC_IFRAME_URL}?${queryParams.join('&')}` + }); + } else if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `${USER_SYNC_PIXEL_URL}?srv=cs&${queryParams.join('&')}` }); - } else { - logWarn('IMDS: Please enable iframe based user sync.'); } + return syncs; } }; diff --git a/modules/imdsBidAdapter.md b/modules/imdsBidAdapter.md index 15fb407e7ef..2a50868d726 100644 --- a/modules/imdsBidAdapter.md +++ b/modules/imdsBidAdapter.md @@ -11,11 +11,11 @@ Maintainer: eng-demand@imds.tv The iMedia Digital Services adapter requires setup and approval from iMedia Digital Services. Please reach out to your account manager for more information. -### DFP Video Creative -To use video, setup a `VAST redirect` creative within Google AdManager (DFP) with the following VAST tag URL: +### Google Ad Manager Video Creative +To use video, setup a `VAST redirect` creative within Google Ad Manager with the following VAST tag URL: -``` -https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm%%&AUCTION_PRICE=%%PATTERN:hb_pb_synacormedia%% +```text +https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_uuid_imds%%&AUCTION_PRICE=%%PATTERN:hb_pb_imds%% ``` # Test Parameters diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index f2bf9aaddcb..ea446bd150d 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -1,7 +1,18 @@ -import {deepAccess, deepSetValue, generateUUID} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {ajax} from '../src/ajax.js'; +'use strict'; + +import { deepAccess, deepSetValue, generateUUID } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const BIDDER_CODE = 'impactify'; const BIDDER_ALIAS = ['imp']; @@ -10,58 +21,125 @@ const DEFAULT_VIDEO_WIDTH = 640; const DEFAULT_VIDEO_HEIGHT = 360; const ORIGIN = 'https://sonic.impactify.media'; const LOGGER_URI = 'https://logger.impactify.media'; -const AUCTIONURI = '/bidder'; -const COOKIESYNCURI = '/static/cookie_sync.html'; -const GVLID = 606; -const GETCONFIG = config.getConfig; - -const getDeviceType = () => { - // OpenRTB Device type - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { - return 5; - } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { - return 4; - } - return 2; -}; +const AUCTION_URI = '/bidder'; +const COOKIE_SYNC_URI = '/static/cookie_sync.html'; +const GVL_ID = 606; +const GET_CONFIG = config.getConfig; +export const STORAGE = getStorageManager({ gvlid: GVL_ID, bidderCode: BIDDER_CODE }); +export const STORAGE_KEY = '_im_str' + +/** + * Helpers object + * @type {{getExtParamsFromBid(*): {impactify: {appId}}, createOrtbImpVideoObj(*): {context: string, playerSize: [number,number], id: string, mimes: [string]}, getDeviceType(): (number), createOrtbImpBannerObj(*, *): {format: [], id: string}}} + */ +const helpers = { + getExtParamsFromBid(bid) { + let ext = { + impactify: { + appId: bid.params.appId + }, + }; -const getFloor = (bid) => { - const floorInfo = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: '*', - size: '*' - }); - if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { - return parseFloat(floorInfo.floor); + if (typeof bid.params.format == 'string') { + ext.impactify.format = bid.params.format; + } + + if (typeof bid.params.style == 'string') { + ext.impactify.style = bid.params.style; + } + + if (typeof bid.params.container == 'string') { + ext.impactify.container = bid.params.container; + } + + if (typeof bid.params.size == 'string') { + ext.impactify.size = bid.params.size; + } + + return ext; + }, + + getDeviceType() { + // OpenRTB Device type + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 5; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 4; + } + return 2; + }, + + createOrtbImpBannerObj(bid, size) { + let sizes = size.split('x'); + + return { + id: 'banner-' + bid.bidId, + format: [{ + w: parseInt(sizes[0]), + h: parseInt(sizes[1]) + }] + } + }, + + createOrtbImpVideoObj(bid) { + return { + id: 'video-' + bid.bidId, + playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], + context: 'outstream', + mimes: ['video/mp4'], + } + }, + + getFloor(bid) { + const floorInfo = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + return parseFloat(floorInfo.floor); + } + return null; + }, + + getImStrFromLocalStorage() { + return STORAGE.localStorageIsEnabled(false) ? STORAGE.getDataFromLocalStorage(STORAGE_KEY, false) : ''; } - return null; + } -const createOpenRtbRequest = (validBidRequests, bidderRequest) => { +/** + * Create an OpenRTB formated object from prebid payload + * @param validBidRequests + * @param bidderRequest + * @returns {{cur: string[], validBidRequests, id, source: {tid}, imp: *[]}} + */ +function createOpenRtbRequest(validBidRequests, bidderRequest) { // Create request and set imp bids inside let request = { id: bidderRequest.bidderRequestId, validBidRequests, cur: [DEFAULT_CURRENCY], imp: [], - source: {tid: bidderRequest.ortb2?.source?.tid} + source: { tid: bidderRequest.ortb2?.source?.tid } }; // Get the url parameters const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const checkPrebid = urlParams.get('_checkPrebid'); - // Force impactify debugging parameter + + // Force impactify debugging parameter if present if (checkPrebid != null) { request.test = Number(checkPrebid); } - // Set Schain in request + // Set SChain in request let schain = deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; - // Set eids + // Set Eids let eids = deepAccess(validBidRequests, '0.userIdAsEids'); if (eids && eids.length) { deepSetValue(request, 'user.ext.eids', eids); @@ -73,13 +151,13 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { request.device = { w: window.innerWidth, h: window.innerHeight, - devicetype: getDeviceType(), + devicetype: helpers.getDeviceType(), ua: navigator.userAgent, js: 1, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, language: ((navigator.language || navigator.userLanguage || '').split('-'))[0] || 'en', }; - request.site = {page: bidderRequest.refererInfo.page}; + request.site = { page: bidderRequest.refererInfo.page }; // Handle privacy settings for GDPR/CCPA/COPPA let gdprApplies = 0; @@ -91,9 +169,10 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + this.syncStore.uspConsent = bidderRequest.uspConsent; } - if (GETCONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); + if (GET_CONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); @@ -104,42 +183,47 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { // Create imps with bids validBidRequests.forEach((bid) => { + let bannerObj = deepAccess(bid.mediaTypes, `banner`); + let imp = { id: bid.bidId, bidfloor: bid.params.bidfloor ? bid.params.bidfloor : 0, - ext: { - impactify: { - appId: bid.params.appId, - format: bid.params.format, - style: bid.params.style - }, - }, - video: { - playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], - context: 'outstream', - mimes: ['video/mp4'], - }, + ext: helpers.getExtParamsFromBid(bid) }; - if (bid.params.container) { - imp.ext.impactify.container = bid.params.container; + + if (bannerObj && typeof imp.ext.impactify.size == 'string') { + imp.banner = { + ...helpers.createOrtbImpBannerObj(bid, imp.ext.impactify.size) + } + } else { + imp.video = { + ...helpers.createOrtbImpVideoObj(bid) + } } + if (typeof bid.getFloor === 'function') { - const floor = getFloor(bid); + const floor = helpers.getFloor(bid); if (floor) { imp.bidfloor = floor; } } + request.imp.push(imp); }); return request; -}; +} +/** + * Export BidderSpec type object and register it to Prebid + * @type {{supportedMediaTypes: string[], interpretResponse: ((function(ServerResponse, *): Bid[])|*), code: string, aliases: string[], getUserSyncs: ((function(SyncOptions, ServerResponse[], *, *): UserSync[])|*), buildRequests: (function(*, *): {method: string, data: string, url}), onTimeout: (function(*): boolean), gvlid: number, isBidRequestValid: ((function(BidRequest): (boolean))|*), onBidWon: (function(*): boolean)}} + */ export const spec = { code: BIDDER_CODE, - gvlid: GVLID, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], aliases: BIDDER_ALIAS, + storageAllowed: true, /** * Determines whether or not the given bid request is valid. @@ -148,13 +232,16 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!bid.params.appId || typeof bid.params.appId != 'string' || !bid.params.format || typeof bid.params.format != 'string' || !bid.params.style || typeof bid.params.style != 'string') { + if (typeof bid.params.appId != 'string' || !bid.params.appId) { return false; } - if (bid.params.format != 'screen' && bid.params.format != 'display') { + if (typeof bid.params.format != 'string' || typeof bid.params.style != 'string' || !bid.params.format || !bid.params.style) { return false; } - if (bid.params.style != 'inline' && bid.params.style != 'impact' && bid.params.style != 'static') { + if (bid.params.format !== 'screen' && bid.params.format !== 'display') { + return false; + } + if (bid.params.style !== 'inline' && bid.params.style !== 'impact' && bid.params.style !== 'static') { return false; } @@ -171,11 +258,20 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // Create a clean openRTB request let request = createOpenRtbRequest(validBidRequests, bidderRequest); + const imStr = helpers.getImStrFromLocalStorage(); + const options = {} + + if (imStr) { + options.customHeaders = { + 'x-impact': imStr + }; + } return { method: 'POST', - url: ORIGIN + AUCTIONURI, + url: ORIGIN + AUCTION_URI, data: JSON.stringify(request), + options }; }, @@ -265,16 +361,16 @@ export const spec = { return [{ type: 'iframe', - url: ORIGIN + COOKIESYNCURI + params + url: ORIGIN + COOKIE_SYNC_URI + params }]; }, /** * Register bidder specific code, which will execute if a bid from this bidder won the auction * @param {Bid} The bid that won the auction - */ - onBidWon: function(bid) { - ajax(`${LOGGER_URI}/log/bidder/won`, null, JSON.stringify(bid), { + */ + onBidWon: function (bid) { + ajax(`${LOGGER_URI}/prebid/won`, null, JSON.stringify(bid), { method: 'POST', contentType: 'application/json' }); @@ -285,9 +381,9 @@ export const spec = { /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data - */ - onTimeout: function(data) { - ajax(`${LOGGER_URI}/log/bidder/timeout`, null, JSON.stringify(data[0]), { + */ + onTimeout: function (data) { + ajax(`${LOGGER_URI}/prebid/timeout`, null, JSON.stringify(data[0]), { method: 'POST', contentType: 'application/json' }); diff --git a/modules/impactifyBidAdapter.md b/modules/impactifyBidAdapter.md index 3de9a8cfb84..de3373395dc 100644 --- a/modules/impactifyBidAdapter.md +++ b/modules/impactifyBidAdapter.md @@ -10,14 +10,22 @@ Maintainer: thomas.destefano@impactify.io Module that connects to the Impactify solution. The impactify bidder need 3 parameters: - - appId : This is your unique publisher identifier - - format : This is the ad format needed, can be : screen or display - - style : This is the ad style needed, can be : inline, impact or static +- appId : This is your unique publisher identifier +- format : This is the ad format needed, can be : screen or display (Only for video media type) +- style : This is the ad style needed, can be : inline, impact or static (Only for video media type) + +Note : Impactify adapter need storage access to work properly (Do not forget to set storageAllowed to true). # Test Parameters ``` - var adUnits = [{ - code: 'your-slot-div-id', // This is your slot div id + pbjs.bidderSettings = { + impactify: { + storageAllowed: true // Mandatory + } + }; + + var adUnitsVideo = [{ + code: 'your-slot-div-id-video', // This is your slot div id mediaTypes: { video: { context: 'outstream' @@ -32,4 +40,24 @@ The impactify bidder need 3 parameters: } }] }]; + + var adUnitsBanner = [{ + code: 'your-slot-div-id-banner', // This is your slot div id + mediaTypes: { + banner: { + sizes: [ + [728, 90] + ] + } + }, + bids: [{ + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'display', + size: '728x90', + style: 'static' + } + }] + }]; ``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index b56cc56a186..3a258dfa327 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -7,6 +7,14 @@ import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {loadExternalScript} from '../src/adloader.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'improvedigital'; const CREATIVE_TTL = 300; @@ -182,6 +190,10 @@ export const CONVERTER = ortbConverter({ })(); const bidResponse = buildBidResponse(bid, context); const idExt = deepAccess(bid, `ext.${BIDDER_CODE}`, {}); + // Programmatic guaranteed flag + if (idExt.pg === 1) { + bidResponse.adserverTargeting = { hb_deal_type_improve: 'pg' }; + } Object.assign(bidResponse, { dealId: (typeof idExt.buying_type === 'string' && idExt.buying_type !== 'rtb') ? idExt.line_item_id : undefined, netRevenue: idExt.is_net || false, diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 38870c9403b..1242ca183ea 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -11,6 +11,11 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'imuid'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/incrxBidAdapter.js b/modules/incrxBidAdapter.js index d054309ee40..9b939aff11b 100644 --- a/modules/incrxBidAdapter.js +++ b/modules/incrxBidAdapter.js @@ -2,6 +2,12 @@ import { parseSizesInput, isEmpty } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'incrementx'; const ENDPOINT_URL = 'https://hb.incrementxserv.com/vzhbidder/bid'; const DEFAULT_CURRENCY = 'USD'; @@ -12,22 +18,22 @@ export const spec = { 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. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function (bid) { return !!(bid.params.placementId); }, /** - * Make a server request from the list of BidRequests. - * - * @param validBidRequests - * @param bidderRequest - * @return Array Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param validBidRequests + * @param bidderRequest + * @return Array Info describing the request to the server. + */ buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); @@ -53,11 +59,11 @@ export const spec = { }, /** - * 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. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function (serverResponse) { const response = serverResponse.body; const bids = []; diff --git a/modules/innityBidAdapter.js b/modules/innityBidAdapter.js index 99eec210193..9bd0538ff0a 100644 --- a/modules/innityBidAdapter.js +++ b/modules/innityBidAdapter.js @@ -38,6 +38,9 @@ export const spec = { }, interpretResponse: function(serverResponse, request) { const res = serverResponse.body; + if (Object.keys(res).length === 0) { + return []; + } const bidResponse = { requestId: res.callback_uid, cpm: parseFloat(res.cpm) / 100, diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index c770ac69dbe..4d9b95e5948 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, generateUUID, logError, isArray} from '../src/utils.js'; +import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {find} from '../src/polyfill.js'; @@ -12,6 +12,35 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days const BID_TTL = 300; // 5 minutes const GVLID = 910; +const isSubarray = (arr, target) => { + if (!isArrayOfNums(arr) || arr.length === 0) { + return false; + } + const targetSet = new Set(target); + return arr.every(el => targetSet.has(el)); +}; + +export const OPTIONAL_VIDEO_PARAMS = { + 'minduration': (value) => isInteger(value), + 'maxduration': (value) => isInteger(value), + 'protocols': (value) => isSubarray(value, [2, 3, 5, 6, 7, 8]), // protocols values supported by Inticator, according to the OpenRTB spec + 'startdelay': (value) => isInteger(value), + 'linearity': (value) => isInteger(value) && [1].includes(value), + 'skip': (value) => isInteger(value) && [1, 0].includes(value), + 'skipmin': (value) => isInteger(value), + 'skipafter': (value) => isInteger(value), + 'sequence': (value) => isInteger(value), + 'battr': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]), + 'maxextended': (value) => isInteger(value), + 'minbitrate': (value) => isInteger(value), + 'maxbitrate': (value) => isInteger(value), + 'playbackmethod': (value) => isSubarray(value, [1, 2, 3, 4]), + 'playbackend': (value) => isInteger(value) && [1, 2, 3].includes(value), + 'delivery': (value) => isSubarray(value, [1, 2, 3]), + 'pos': (value) => isInteger(value) && [0, 1, 2, 3, 4, 5, 6, 7].includes(value), + 'api': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7]), +}; + export const storage = getStorageManager({bidderCode: BIDDER_CODE}); config.setDefaults({ @@ -68,17 +97,51 @@ function buildBanner(bidRequest) { } function buildVideo(bidRequest) { - const w = deepAccess(bidRequest, 'mediaTypes.video.w'); - const h = deepAccess(bidRequest, 'mediaTypes.video.h'); + let w = deepAccess(bidRequest, 'mediaTypes.video.w'); + let h = deepAccess(bidRequest, 'mediaTypes.video.h'); const mimes = deepAccess(bidRequest, 'mediaTypes.video.mimes'); const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3; + const plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt') || undefined; + const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } - return { + const bidRequestVideo = deepAccess(bidRequest, 'mediaTypes.video'); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + let optionalParams = {}; + for (const param in OPTIONAL_VIDEO_PARAMS) { + if (bidRequestVideo[param]) { + optionalParams[param] = bidRequestVideo[param]; + } + } + + if (plcmt) { + optionalParams['plcmt'] = plcmt; + } + + let videoObj = { placement, mimes, w, h, + ...optionalParams, + ...videoBidderParams // bidder specific overrides for video } + + return videoObj } function buildImpression(bidRequest) { @@ -106,8 +169,10 @@ function buildImpression(bidRequest) { return imp; } -function buildDevice() { - const deviceConfig = config.getConfig('device'); +function buildDevice(bidRequest) { + const ortb2Data = bidRequest?.ortb2 || {}; + const deviceConfig = ortb2Data?.device || {} + const device = { w: window.innerWidth, h: window.innerHeight, @@ -184,7 +249,7 @@ function buildRequest(validBidRequests, bidderRequest) { page: bidderRequest.refererInfo.page, ref: bidderRequest.refererInfo.ref, }, - device: buildDevice(), + device: buildDevice(bidderRequest), regs: buildRegs(bidderRequest), user: buildUser(validBidRequests[0]), imp: validBidRequests.map((bidRequest) => buildImpression(bidRequest)), @@ -233,7 +298,11 @@ function buildBid(bid, bidderRequest) { meta.advertiserDomains = bid.adomain } - return { + let mediaType = 'banner'; + if (bid.adm && bid.adm.includes(' 0 ? {meta} : {}) }; + + if (mediaType === 'video') { + bidResponse.vastXml = bid.adm; + } + + // Inticator bid adaptor only returns `vastXml` for video bids. No VastUrl or videoCache. + if (!bidResponse.vastUrl && bidResponse.vastXml) { + bidResponse.vastUrl = 'data:text/xml;charset=utf-8;base64,' + window.btoa(bidResponse.vastXml.replace(/\\"/g, '"')); + } + + return bidResponse; } function buildBidSet(seatbid, bidderRequest) { @@ -307,15 +387,38 @@ function validateBanner(bid) { } function validateVideo(bid) { - const video = deepAccess(bid, 'mediaTypes.video'); + const videoParams = deepAccess(bid, 'mediaTypes.video'); + const videoBidderParams = deepAccess(bid, 'params.video'); + let video = { + ...videoParams, + ...videoBidderParams // bidder specific overrides for video + } - if (video === undefined) { + // Check if the video object is undefined + if (videoParams === undefined) { return true; } + let w = deepAccess(bid, 'mediaTypes.video.w'); + let h = deepAccess(bid, 'mediaTypes.video.h'); + const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } const videoSize = [ - deepAccess(bid, 'mediaTypes.video.w'), - deepAccess(bid, 'mediaTypes.video.h'), + w, + h, ]; if ( @@ -339,6 +442,27 @@ function validateVideo(bid) { return false; } + const plcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); + + if (typeof plcmt !== 'undefined' && typeof plcmt !== 'number') { + logError('insticator: video plcmt is not a number'); + return false; + } + + for (const param in OPTIONAL_VIDEO_PARAMS) { + if (video[param]) { + if (!OPTIONAL_VIDEO_PARAMS[param](video[param])) { + logError(`insticator: video ${param} is invalid or not supported by insticator`); + return false + } + } + } + + if (video.minduration && video.maxduration && video.minduration > video.maxduration) { + logError('insticator: video minduration is greater than maxduration'); + return false; + } + return true; } @@ -380,7 +504,6 @@ export const spec = { interpretResponse: function (serverResponse, request) { const bidderRequest = request.bidderRequest; const body = serverResponse.body; - if (!body || body.id !== bidderRequest.bidderRequestId) { logError('insticator: response id does not match bidderRequestId'); return []; diff --git a/modules/instreamTracking.js b/modules/instreamTracking.js index ff8305c7fed..ece556d0fd2 100644 --- a/modules/instreamTracking.js +++ b/modules/instreamTracking.js @@ -5,6 +5,12 @@ import { INSTREAM } from '../src/video.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').AdUnit} AdUnit + */ + const {CACHE_ID, UUID} = CONSTANTS.TARGETING_KEYS; const {BID_WON, AUCTION_END} = CONSTANTS.EVENTS; const {RENDERED} = CONSTANTS.BID_STATUS; diff --git a/modules/integr8BidAdapter.js b/modules/integr8BidAdapter.js index a85e9b0a55c..949483ea7bf 100644 --- a/modules/integr8BidAdapter.js +++ b/modules/integr8BidAdapter.js @@ -3,13 +3,20 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'integr8'; -const ENDPOINT_URL = 'https://integr8.central.gjirafa.tech/bid'; +const DEFAULT_ENDPOINT_URL = 'https://central.sea.integr8.digital/bid'; const DIMENSION_SEPARATOR = 'x'; const SIZE_SEPARATOR = ';'; -const BISKO_ID = 'biskoId'; +const BISKO_ID = 'integr8Id'; const STORAGE_ID = 'bisko-sid'; -const SEGMENTS = 'biskoSegments'; +const SEGMENTS = 'integr8Segments'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { @@ -31,6 +38,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + let deliveryUrl = ''; const storageId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(STORAGE_ID) || '' : ''; const biskoId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(BISKO_ID) || '' : ''; const segments = storage.localStorageIsEnabled() ? JSON.parse(storage.getDataFromLocalStorage(SEGMENTS)) || [] : []; @@ -55,6 +63,9 @@ export const spec = { if (!pageViewGuid) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } if (!Object.keys(data).length && bidRequest.params.data && Object.keys(bidRequest.params.data).length) { data = bidRequest.params.data; } + if (!deliveryUrl && bidRequest.params && typeof bidRequest.params.deliveryUrl === 'string') { + deliveryUrl = bidRequest.params.deliveryUrl; + } return { sizes: generateSizeParam(bidRequest.sizes), @@ -67,6 +78,10 @@ export const spec = { }; }); + if (!deliveryUrl) { + deliveryUrl = DEFAULT_ENDPOINT_URL; + } + let body = { propertyId: propertyId, pageViewGuid: pageViewGuid, @@ -82,7 +97,7 @@ export const spec = { return [{ method: 'POST', - url: ENDPOINT_URL, + url: deliveryUrl, data: body }]; }, @@ -120,11 +135,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/integr8BidAdapter.md b/modules/integr8BidAdapter.md index eadab7acdb3..da52a2164c6 100644 --- a/modules/integr8BidAdapter.md +++ b/modules/integr8BidAdapter.md @@ -3,7 +3,7 @@ Module Name: Integr8 Bidder Adapter Module Type: Bidder Adapter -Maintainer: arditb@gjirafa.com +Maintainer: myhedin@gjirafa.com # Description Integr8 Bidder Adapter for Prebid.js. @@ -23,8 +23,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846835', //Required + propertyId: '105135', //Required + placementId: '846837', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", @@ -48,8 +49,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846830', //Required + propertyId: '105135', //Required + placementId: '846835', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 5164080c317..109ea5c39c6 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -11,6 +11,12 @@ import { submodule } from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const PCID_EXPIRY = 365; const MODULE_NAME = 'intentIqId'; diff --git a/modules/invamiaBidAdapter.js b/modules/invamiaBidAdapter.js index 2d36fb77e16..96af163ca4f 100644 --- a/modules/invamiaBidAdapter.js +++ b/modules/invamiaBidAdapter.js @@ -1,6 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'invamia'; const ENDPOINT_URL = 'https://ad.invamia.com/delivery/impress'; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 0c0d1cdef87..2c37c0edad9 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -2,6 +2,11 @@ import {logInfo} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const CONSTANTS = { BIDDER_CODE: 'invibes', BID_ENDPOINT: '.videostep.com/Bid/VideoAdContent', @@ -9,7 +14,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 10, + PREBID_VERSION: 11, METHOD: 'GET', INVIBES_VENDOR_ID: 436, USERID_PROVIDERS: ['pubcid', 'pubProvidedId', 'uid2', 'zeotapIdPlus', 'id5id'], @@ -49,14 +54,36 @@ registerBidder(spec); // some state info is required: cookie info, unique user visit id const topWin = getTopMostWindow(); let invibes = topWin.invibes = topWin.invibes || {}; -invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false]; -invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false]; +invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false, false]; +invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false, false]; invibes.placementBids = invibes.placementBids || []; invibes.pushedCids = invibes.pushedCids || {}; let preventPageViewEvent = false; +let isInfiniteScrollPage = false; +let isPlacementRefresh = false; let _customUserSync; let _disableUserSyncs; +function updateInfiniteScrollFlag() { + const { scrollHeight } = document.documentElement; + + if (invibes.originalURL === undefined) { + invibes.originalURL = window.location.href; + return; + } + + if (invibes.originalScrollHeight === undefined) { + invibes.originalScrollHeight = scrollHeight; + return; + } + + const currentURL = window.location.href; + + if (scrollHeight > invibes.originalScrollHeight && invibes.originalURL !== currentURL) { + isInfiniteScrollPage = true; + } +} + function isBidRequestValid(bid) { if (typeof bid.params !== 'object') { return false; @@ -87,10 +114,24 @@ function buildRequest(bidRequests, bidderRequest) { const _placementIds = []; const _adUnitCodes = []; let _customEndpoint, _userId, _domainId; - let _ivAuctionStart = bidderRequest.auctionStart || Date.now(); + let _ivAuctionStart = Date.now(); + window.invibes = window.invibes || {}; + window.invibes.placementIds = window.invibes.placementIds || []; + + if (isInfiniteScrollPage == false) { + updateInfiniteScrollFlag(); + } bidRequests.forEach(function (bidRequest) { bidRequest.startTime = new Date().getTime(); + + if (window.invibes.placementIds.includes(bidRequest.params.placementId)) { + isPlacementRefresh = true; + } + + window.invibes.placementIds.push(bidRequest.params.placementId); + + _placementIds.push(bidRequest.params.placementId); _placementIds.push(bidRequest.params.placementId); _adUnitCodes.push(bidRequest.adUnitCode); _domainId = _domainId || bidRequest.params.domainId; @@ -138,6 +179,8 @@ function buildRequest(bidRequests, bidderRequest) { tc: invibes.gdpr_consent, isLocalStorageEnabled: storage.hasLocalStorage(), preventPageViewEvent: preventPageViewEvent, + isPlacementRefresh: isPlacementRefresh, + isInfiniteScrollPage: isInfiniteScrollPage, }; let lid = readFromLocalStorage('ivbsdid'); @@ -368,7 +411,9 @@ function addMeta(bidModelMeta) { } function generateRandomId() { - return (Math.round(Math.random() * 1e12)).toString(36).substring(0, 10); + return '10000000100040008000100000000000'.replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); } function getDocumentLocation(bidderRequest) { @@ -568,7 +613,7 @@ function readGdprConsent(gdprConsent) { } let legitimateInterests = getLegitimateInterests(gdprConsent.vendorData); - tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, 10); + tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, purposesLength); let invibesVendorId = CONSTANTS.INVIBES_VENDOR_ID.toString(10); let vendorConsents = getVendorConsents(gdprConsent.vendorData); @@ -621,6 +666,10 @@ function tryCopyValueToArray(value, target, length) { function getPurposeConsentsCounter(vendorData) { if (vendorData.purpose && vendorData.purpose.consents) { + if (vendorData.tcfPolicyVersion >= 4) { + return 11; + } + return 10; } diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index eaf20ad3ad3..1188af471a7 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -3,13 +3,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'iprom'; const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; -const VERSION = 'v1.0.2'; +const VERSION = 'v1.0.3'; const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; +const IAB_GVL_ID = 811; export const spec = { code: BIDDER_CODE, + gvlid: IAB_GVL_ID, isBidRequestValid: function ({ bidder, params = {} } = {}) { // id parameter checks if (!params.id) { diff --git a/modules/iqmBidAdapter.js b/modules/iqmBidAdapter.js index c3808afd225..c94a88748a7 100644 --- a/modules/iqmBidAdapter.js +++ b/modules/iqmBidAdapter.js @@ -3,6 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {INSTREAM} from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'iqm'; const VERSION = 'v.1.0.0'; const VIDEO_ORTB_PARAMS = [ diff --git a/modules/iqxBidAdapter.js b/modules/iqxBidAdapter.js new file mode 100644 index 00000000000..1bef158c4a2 --- /dev/null +++ b/modules/iqxBidAdapter.js @@ -0,0 +1,207 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +const CUR = 'USD'; +const BIDDER_CODE = 'iqx'; +const ENDPOINT = 'https://pbjs.iqzonertb.live'; + +/** + * 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. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['iqx'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/iqxBidAdapter.md b/modules/iqxBidAdapter.md new file mode 100644 index 00000000000..c48864c4306 --- /dev/null +++ b/modules/iqxBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: IQX Bidder Adapter +Module Type: IQX Bidder Adapter +Maintainer: it@iqzone.com +``` + +# Description + +Module that connects to iqx.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/ivsBidAdapter.js b/modules/ivsBidAdapter.js index 47685fbbe46..3deebf9bff3 100644 --- a/modules/ivsBidAdapter.js +++ b/modules/ivsBidAdapter.js @@ -1,9 +1,16 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { deepAccess, deepSetValue, getBidIdParameter, logError } from '../src/utils.js'; +import {deepAccess, deepSetValue, getBidIdParameter, logError} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { INSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'ivs'; const ENDPOINT_URL = 'https://a.ivstracker.net/prod/openrtb/2.5'; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 5e39c43367f..a29c1a39bff 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,15 +1,16 @@ import { contains, - convertTypes, deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, inIframe, isArray, isEmpty, isFn, isInteger, + isNumber, + isStr, + isPlainObject, logError, logWarn, mergeDeep, @@ -24,6 +25,8 @@ import { find } from '../src/polyfill.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -38,6 +41,7 @@ const VIDEO_TIME_TO_LIVE = 3600; // 1hr const NATIVE_TIME_TO_LIVE = 3600; // Since native can have video, use ttl same as video const NET_REVENUE = true; const MAX_REQUEST_LIMIT = 4; +const MAX_EID_SOURCES = 50; const OUTSTREAM_MINIMUM_PLAYER_SIZE = [144, 144]; const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 @@ -76,9 +80,12 @@ const SOURCE_RTI_MAPPING = { 'audigent.com': '', // Hadron ID from Audigent, hadronId 'pubcid.org': '', // SharedID, pubcid 'utiq.com': '', // Utiq + 'criteo.com': '', // Criteo + 'euid.eu': '', // EUID 'intimatemerger.com': '', '33across.com': '', 'liveintent.indexexchange.com': '', + 'google.com': '' }; const PROVIDERS = [ 'britepoolid', @@ -89,7 +96,8 @@ const PROVIDERS = [ 'connectid', 'tapadId', 'quantcastId', - 'pubProvidedId' + 'pubProvidedId', + 'pairId' ]; const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration']; // note: protocol/protocols is also reqd const VIDEO_PARAMS_ALLOW_LIST = [ @@ -107,7 +115,8 @@ export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { // Update with list of CFTs to be requested from Exchange REQUESTED_FEATURE_TOGGLES: [ - 'pbjs_enable_multiformat' + 'pbjs_enable_multiformat', + 'pbjs_allow_all_eids' ], featureToggles: {}, @@ -168,6 +177,7 @@ const MEDIA_TYPES = { function bidToBannerImp(bid) { const imp = bidToImp(bid, BANNER); imp.banner = {}; + imp.adunitCode = bid.adUnitCode; const impSize = deepAccess(bid, 'params.size'); if (impSize) { imp.banner.w = impSize[0]; @@ -233,7 +243,10 @@ export function bidToVideoImp(bid) { imp.video = videoParamRef ? deepClone(bid.params.video) : {}; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } setDisplayManager(imp, bid); @@ -323,7 +336,10 @@ export function bidToNativeImp(bid) { }; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } // AdUnit-Specific First Party Data addAdUnitFPD(imp, bid) @@ -343,27 +359,30 @@ function bidToImp(bid, mediaType) { imp.id = bid.bidId; - imp.ext = {}; - + if (isExchangeIdConfigured() && deepAccess(bid, `params.externalId`)) { + deepSetValue(imp, 'ext.externalID', bid.params.externalId); + } if (deepAccess(bid, `params.${mediaType}.siteId`) && !isNaN(Number(bid.params[mediaType].siteId))) { switch (mediaType) { case BANNER: - imp.ext.siteID = bid.params.banner.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.banner.siteId.toString()); break; case VIDEO: - imp.ext.siteID = bid.params.video.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.video.siteId.toString()); break; case NATIVE: - imp.ext.siteID = bid.params.native.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.native.siteId.toString()); break; } } else { - imp.ext.siteID = bid.params.siteId.toString(); + if (bid.params.siteId) { + deepSetValue(imp, 'ext.siteID', bid.params.siteId.toString()); + } } // populate imp level sid if (bid.params.hasOwnProperty('id') && (typeof bid.params.id === 'string' || typeof bid.params.id === 'number')) { - imp.ext.sid = String(bid.params.id); + deepSetValue(imp, 'ext.sid', String(bid.params.id)); } return imp; @@ -409,12 +428,12 @@ function _applyFloor(bid, imp, mediaType) { if (moduleFloor) { imp.bidfloor = moduleFloor.floor; imp.bidfloorcur = moduleFloor.currency; - imp.ext.fl = FLOOR_SOURCE.PBJS; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.PBJS); setFloor = true; } else if (adapterFloor) { imp.bidfloor = adapterFloor.floor; imp.bidfloorcur = adapterFloor.currency; - imp.ext.fl = FLOOR_SOURCE.IX; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.IX); setFloor = true; } @@ -504,6 +523,9 @@ function parseBid(rawBid, currency, bidRequest) { if (rawBid.adomain && rawBid.adomain.length > 0) { bid.meta.advertiserDomains = rawBid.adomain; } + if (rawBid.ext?.dsa) { + bid.meta.dsa = rawBid.ext.dsa + } return bid; } @@ -521,7 +543,7 @@ function isValidSize(size) { * Determines whether or not the given size object is an element of the size * array. * - * @param {array} sizeArray The size array. + * @param {Array} sizeArray The size array. * @param {object} size The size object. * @return {boolean} True if the size object is an element of the size array, and false * otherwise. @@ -576,7 +598,7 @@ function checkVideoParams(mediaTypeVideoRef, paramsVideoRef) { * Get One size from Size Array * [[250,350]] -> [250, 350] * [250, 350] -> [250, 350] - * @param {array} sizes array of sizes + * @param {Array} sizes array of sizes */ function getFirstSize(sizes = []) { if (isValidSize(sizes)) { @@ -593,7 +615,7 @@ function getFirstSize(sizes = []) { * * @param {number} bidFloor The bidFloor parameter inside bid request config. * @param {number} bidFloorCur The bidFloorCur parameter inside bid request config. - * @return {bool} True if this is a valid bidFloor parameters format, and false + * @return {boolean} True if this is a valid bidFloor parameters format, and false * otherwise. */ function isValidBidFloorParams(bidFloor, bidFloorCur) { @@ -616,7 +638,7 @@ function nativeMediaTypeValid(bid) { * Get bid request object with the associated id. * * @param {*} id Id of the impression. - * @param {array} impressions List of impressions sent in the request. + * @param {Array} impressions List of impressions sent in the request. * @return {object} The impression with the associated id. */ function getBidRequest(id, impressions, validBidRequests) { @@ -634,7 +656,7 @@ function getBidRequest(id, impressions, validBidRequests) { /** * From the userIdAsEids array, filter for the ones our adserver can use, and modify them * for our purposes, e.g. add rtiPartner - * @param {array} allEids userIdAsEids passed in by prebid + * @param {Array} allEids userIdAsEids passed in by prebid * @return {object} contains toSend (eids to send to the adserver) and seenSources (used to filter * identity info from IX Library) */ @@ -643,15 +665,23 @@ function getEidInfo(allEids) { let seenSources = {}; if (isArray(allEids)) { for (const eid of allEids) { - if (SOURCE_RTI_MAPPING.hasOwnProperty(eid.source) && deepAccess(eid, 'uids.0')) { + const isSourceMapped = SOURCE_RTI_MAPPING.hasOwnProperty(eid.source); + const allowAllEidsFeatureEnabled = FEATURE_TOGGLES.isFeatureEnabled('pbjs_allow_all_eids'); + const hasUids = deepAccess(eid, 'uids.0'); + + if ((isSourceMapped || allowAllEidsFeatureEnabled) && hasUids) { seenSources[eid.source] = true; - if (SOURCE_RTI_MAPPING[eid.source] != '') { + + if (isSourceMapped && SOURCE_RTI_MAPPING[eid.source] !== '') { eid.uids[0].ext = { rtiPartner: SOURCE_RTI_MAPPING[eid.source] }; } delete eid.uids[0].atype; toSend.push(eid); + if (toSend.length >= MAX_EID_SOURCES) { + break; + } } } } @@ -662,11 +692,11 @@ function getEidInfo(allEids) { /** * Builds a request object to be sent to the ad server based on bid requests. * - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest An object containing other info like gdprConsent. * @param {object} impressions An object containing a list of impression objects describing the bids for each transaction - * @param {array} version Endpoint version denoting banner, video or native. - * @return {array} List of objects describing the request to the server. + * @param {Array} version Endpoint version denoting banner, video or native. + * @return {Array} List of objects describing the request to the server. * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { @@ -694,8 +724,9 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = addRequestedFeatureToggles(r, FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES) // getting ixdiags for adunits of the video, outstream & multi format (MF) style - let ixdiag = buildIXDiag(validBidRequests); - for (var key in ixdiag) { + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + let ixdiag = buildIXDiag(validBidRequests, fledgeEnabled); + for (let key in ixdiag) { r.ext.ixdiag[key] = ixdiag[key]; } @@ -704,8 +735,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = applyRegulations(r, bidderRequest); let payload = {}; - siteID = validBidRequests[0].params.siteId; - payload.s = siteID; + if (validBidRequests[0].params.siteId) { + siteID = validBidRequests[0].params.siteId; + payload.s = siteID; + } const impKeys = Object.keys(impressions); let isFpdAdded = false; @@ -736,13 +769,24 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { const isLastAdUnit = adUnitIndex === impKeys.length - 1; + r = addDeviceInfo(r); r = deduplicateImpExtFields(r); r = removeSiteIDs(r); if (isLastAdUnit) { + let exchangeUrl = `${baseUrl}?`; + + if (siteID !== 0) { + exchangeUrl += `s=${siteID}`; + } + + if (isExchangeIdConfigured()) { + exchangeUrl += siteID !== 0 ? '&' : ''; + exchangeUrl += `p=${config.getConfig('exchangeId')}`; + } requests.push({ method: 'POST', - url: baseUrl + '?s=' + siteID, + url: exchangeUrl, data: deepClone(r), option: { contentType: 'text/plain', @@ -761,13 +805,16 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { /** * addRTI adds RTI info of the partner to retrieved user IDs from prebid ID module. * - * @param {array} userEids userEids info retrieved from prebid - * @param {array} eidInfo eidInfo info from prebid + * @param {Array} userEids userEids info retrieved from prebid + * @param {Array} eidInfo eidInfo info from prebid */ function addRTI(userEids, eidInfo) { let identityInfo = window.headertag.getIdentityInfo(); if (identityInfo && typeof identityInfo === 'object') { for (const partnerName in identityInfo) { + if (userEids.length >= MAX_EID_SOURCES) { + return + } if (identityInfo.hasOwnProperty(partnerName)) { let response = identityInfo[partnerName]; if (!response.responsePending && response.data && typeof response.data === 'object' && @@ -781,7 +828,7 @@ function addRTI(userEids, eidInfo) { /** * createRequest creates the base request object - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @return {object} Object describing the request to the server. */ function createRequest(validBidRequests) { @@ -821,9 +868,9 @@ function addRequestedFeatureToggles(r, requestedFeatureToggles) { * * @param {object} r Base reuqest object. * @param {object} bidderRequest An object containing other info like gdprConsent. - * @param {array} impressions A list of impressions to be added to the request. - * @param {array} validBidRequests A list of valid bid request config objects. - * @param {array} userEids User ID info retrieved from Prebid ID module. + * @param {Array} impressions A list of impressions to be added to the request. + * @param {Array} validBidRequests A list of valid bid request config objects. + * @param {Array} userEids User ID info retrieved from Prebid ID module. * @return {object} Enriched object describing the request to the server. */ function enrichRequest(r, bidderRequest, impressions, validBidRequests, userEids) { @@ -930,10 +977,10 @@ function applyRegulations(r, bidderRequest) { /** * addImpressions adds impressions to request object * - * @param {array} impressions List of impressions to be added to the request. - * @param {array} impKeys List of impression keys. + * @param {Array} impressions List of impressions to be added to the request. + * @param {Array} impKeys List of impression keys. * @param {object} r Reuqest object. - * @param {int} adUnitIndex Index of the current add unit + * @param {number} adUnitIndex Index of the current add unit * @return {object} Reqyest object with added impressions describing the request to the server. */ function addImpressions(impressions, impKeys, r, adUnitIndex) { @@ -949,20 +996,22 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { const dfpAdUnitCode = impressions[impKeys[adUnitIndex]].dfp_ad_unit_code; const tid = impressions[impKeys[adUnitIndex]].tid; const sid = impressions[impKeys[adUnitIndex]].sid; + const auctionEnvironment = impressions[impKeys[adUnitIndex]].ae; const bannerImpressions = impressionObjects.filter(impression => BANNER in impression); const otherImpressions = impressionObjects.filter(impression => !(BANNER in impression)); if (bannerImpressions.length > 0) { const bannerImpsKeyed = bannerImpressions.reduce((acc, bannerImp) => { - if (!acc[bannerImp.id]) { - acc[bannerImp.id] = [] + if (!acc[bannerImp.adunitCode]) { + acc[bannerImp.adunitCode] = [] } - acc[bannerImp.id].push(bannerImp); + acc[bannerImp.adunitCode].push(bannerImp); return acc; }, {}); for (const impId in bannerImpsKeyed) { const bannerImps = bannerImpsKeyed[impId]; const { id, banner: { topframe } } = bannerImps[0]; + let externalID = deepAccess(bannerImps[0], 'ext.externalID'); const _bannerImpression = { id, banner: { @@ -972,15 +1021,24 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { }; for (let i = 0; i < _bannerImpression.banner.format.length; i++) { - // We add sid in imp.ext.sid therefore, remove from banner.format[].ext - if (_bannerImpression.banner.format[i].ext != null && _bannerImpression.banner.format[i].ext.sid != null) { - delete _bannerImpression.banner.format[i].ext.sid; + // We add sid and externalID in imp.ext therefore, remove from banner.format[].ext + if (_bannerImpression.banner.format[i].ext != null) { + if (_bannerImpression.banner.format[i].ext.sid != null) { + delete _bannerImpression.banner.format[i].ext.sid; + } + if (_bannerImpression.banner.format[i].ext.externalID != null) { + delete _bannerImpression.banner.format[i].ext.externalID; + } } // add floor per size if ('bidfloor' in bannerImps[i]) { _bannerImpression.banner.format[i].ext.bidfloor = bannerImps[i].bidfloor; } + + if (JSON.stringify(_bannerImpression.banner.format[i].ext) === '{}') { + delete _bannerImpression.banner.format[i].ext; + } } const position = impressions[impKeys[adUnitIndex]].pos; @@ -988,12 +1046,19 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { _bannerImpression.banner.pos = position; } - if (dfpAdUnitCode || gpid || tid || sid) { + if (dfpAdUnitCode || gpid || tid || sid || auctionEnvironment || externalID) { _bannerImpression.ext = {}; + _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; _bannerImpression.ext.gpid = gpid; _bannerImpression.ext.tid = tid; _bannerImpression.ext.sid = sid; + _bannerImpression.ext.externalID = externalID; + + // enable fledge auction + if (auctionEnvironment == 1) { + _bannerImpression.ext.ae = 1; + } } if ('bidfloor' in bannerImps[0]) { @@ -1017,7 +1082,9 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { // Removes imp.ext.bidfloor // Sets imp.ext.siteID to one of the other [video/native].ext.siteid if imp.ext.siteID doesnt exist otherImpressions.forEach(imp => { - deepSetValue(imp, 'ext.gpid', gpid); + if (gpid) { + deepSetValue(imp, 'ext.gpid', gpid); + } if (r.imp.length > 0) { let matchFound = false; r.imp.forEach((rImp, index) => { @@ -1065,7 +1132,7 @@ This function retrieves the page URL and appends first party data query paramete to it without adding duplicate query parameters. Returns original referer URL if no IX FPD exists. @param {Object} bidderRequest - The bidder request object containing information about the bid and the page. @returns {string} - The modified page URL with first party data query parameters appended. -*/ + */ function getIxFirstPartyDataPageUrl (bidderRequest) { // Parse additional runtime configs. const bidderCode = (bidderRequest && bidderRequest.bidderCode) || 'ix'; @@ -1095,7 +1162,7 @@ This function appends the provided query parameters to the given URL without add @param {string} url - The base URL to which query parameters will be appended. @param {Object} params - An object containing key-value pairs of query parameters to append. @returns {string} - The modified URL with the provided query parameters appended. -*/ + */ function appendIXQueryParams(bidderRequest, url, params) { let urlObj; try { @@ -1151,6 +1218,7 @@ function addFPD(bidderRequest, r, fpd, site, user) { } } + // regulations from ortb2 if (fpd.hasOwnProperty('regs') && !bidderRequest.gppConsent) { if (fpd.regs.hasOwnProperty('gpp') && typeof fpd.regs.gpp == 'string') { deepSetValue(r, 'regs.gpp', fpd.regs.gpp) @@ -1159,6 +1227,30 @@ function addFPD(bidderRequest, r, fpd, site, user) { if (fpd.regs.hasOwnProperty('gpp_sid') && Array.isArray(fpd.regs.gpp_sid)) { deepSetValue(r, 'regs.gpp_sid', fpd.regs.gpp_sid) } + + if (fpd.regs.ext?.dsa) { + const pubDsaObj = fpd.regs.ext.dsa; + const dsaObj = {}; + ['dsarequired', 'pubrender', 'datatopub'].forEach((dsaKey) => { + if (isNumber(pubDsaObj[dsaKey])) { + dsaObj[dsaKey] = pubDsaObj[dsaKey]; + } + }); + + if (isArray(pubDsaObj.transparency)) { + const tpData = []; + pubDsaObj.transparency.forEach((tpObj) => { + if (isPlainObject(tpObj) && isStr(tpObj.domain) && tpObj.domain != '' && isArray(tpObj.dsaparams) && tpObj.dsaparams.every((v) => isNumber(v))) { + tpData.push(tpObj); + } + }); + if (tpData.length > 0) { + dsaObj.transparency = tpData; + } + } + + if (!isEmpty(dsaObj)) deepSetValue(r, 'regs.ext.dsa', dsaObj); + } } return r; @@ -1180,10 +1272,10 @@ function addAdUnitFPD(imp, bid) { /** * addIdentifiersInfo adds indentifier info to ixDaig. * - * @param {array} impressions List of impressions to be added to the request. + * @param {Array} impressions List of impressions to be added to the request. * @param {object} r Reuqest object. - * @param {array} impKeys List of impression keys. - * @param {int} adUnitIndex Index of the current add unit + * @param {Array} impKeys List of impression keys. + * @param {number} adUnitIndex Index of the current add unit * @param {object} payload Request payload object. * @param {string} baseUrl Base exchagne URL. * @return {object} Reqyest object with added indentigfier info to ixDiag. @@ -1206,7 +1298,7 @@ function addIdentifiersInfo(impressions, r, impKeys, adUnitIndex, payload, baseU /** * Return an object of user IDs stored by Prebid User ID module * - * @returns {array} ID providers that are present in userIds + * @returns {Array} ID providers that are present in userIds */ function _getUserIds(bidRequest) { const userIds = bidRequest.userId || {}; @@ -1217,15 +1309,17 @@ function _getUserIds(bidRequest) { /** * Calculates IX diagnostics values and packages them into an object * - * @param {array} validBidRequests The valid bid requests from prebid + * @param {Array} validBidRequests - The valid bid requests from prebid + * @param {boolean} fledgeEnabled - Flag indicating if protected audience (fledge) is enabled * @return {Object} IX diag values for ad units */ -function buildIXDiag(validBidRequests) { +function buildIXDiag(validBidRequests, fledgeEnabled) { var adUnitMap = validBidRequests .map(bidRequest => bidRequest.adUnitCode) .filter((value, index, arr) => arr.indexOf(value) === index); - var ixdiag = { + let allEids = deepAccess(validBidRequests, '0.userIdAsEids', []) + let ixdiag = { mfu: 0, bu: 0, iu: 0, @@ -1236,12 +1330,14 @@ function buildIXDiag(validBidRequests) { version: '$prebid.version$', userIds: _getUserIds(validBidRequests[0]), url: window.location.href.split('?')[0], - vpd: defaultVideoPlacement + vpd: defaultVideoPlacement, + ae: fledgeEnabled, + eidLength: allEids.length }; // create ad unit map and collect the required diag properties - for (let i = 0; i < adUnitMap.length; i++) { - var bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnitMap[i])[0]; + for (let adUnit of adUnitMap) { + let bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnit)[0]; if (deepAccess(bid, 'mediaTypes')) { if (Object.keys(bid.mediaTypes).length > 1) { @@ -1277,8 +1373,8 @@ function buildIXDiag(validBidRequests) { /** * - * @param {array} bannerSizeList list of banner sizes - * @param {array} bannerSize the size to be removed + * @param {Array} bannerSizeList list of banner sizes + * @param {Array} bannerSize the size to be removed * @return {boolean} true if successfully removed, false if not found */ @@ -1347,7 +1443,7 @@ function createVideoImps(validBidRequest, videoImps) { * @param {object} missingBannerSizes reference to missing banner config sizes * @param {object} bannerImps reference to created banner impressions */ -function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { +function createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest) { let imp = bidToBannerImp(validBidRequest); const bannerSizeDefined = includesSize(deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), deepAccess(validBidRequest, 'params.size')); @@ -1363,6 +1459,21 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { bannerImps[validBidRequest.adUnitCode].tagId = deepAccess(validBidRequest, 'params.tagId'); bannerImps[validBidRequest.adUnitCode].pos = deepAccess(validBidRequest, 'mediaTypes.banner.pos'); + // Add Fledge flag if enabled + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + if (fledgeEnabled) { + const auctionEnvironment = deepAccess(validBidRequest, 'ortb2Imp.ext.ae') + if (auctionEnvironment) { + if (isInteger(auctionEnvironment)) { + bannerImps[validBidRequest.adUnitCode].ae = auctionEnvironment; + } else { + logWarn('error setting auction environment flag - must be an integer') + } + } else if (deepAccess(bidderRequest, 'defaultForSlots') == 1) { + bannerImps[validBidRequest.adUnitCode].ae = 1 + } + } + // AdUnit-Specific First Party Data const adUnitFPD = deepAccess(validBidRequest, 'ortb2Imp.ext.data'); if (adUnitFPD) { @@ -1422,7 +1533,7 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { /** * @param {object} bid ValidBidRequest object, used to adjust floor * @param {object} imp Impression object to be modified - * @param {array} newSize The new size to be applied + * @param {Array} newSize The new size to be applied * @return {object} newImp Updated impression object */ function createMissingBannerImp(bid, imp, newSize) { @@ -1598,6 +1709,17 @@ function isIndexRendererPreferred(bid) { return !isValid || renderer.backupOnly; } +function isExchangeIdConfigured() { + let exchangeId = config.getConfig('exchangeId'); + if (typeof exchangeId === 'number' && isFinite(exchangeId)) { + return true; + } + if (typeof exchangeId === 'string' && exchangeId.trim() !== '' && isFinite(Number(exchangeId))) { + return true; + } + return false; +} + export const spec = { code: BIDDER_CODE, @@ -1655,14 +1777,21 @@ export const spec = { } } - if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { - logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + if (!isExchangeIdConfigured() && bid.params.siteId == undefined) { + logError('IX Bid Adapter: Invalid configuration - either siteId or exchangeId must be configured.'); return false; } - if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { - logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); - return false; + if (bid.params.siteId !== undefined) { + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { + logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } + + if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { + logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } } if (hasBidFloor || hasBidFloorCur) { @@ -1695,10 +1824,15 @@ export const spec = { return nativeMediaTypeValid(bid); }, + // For testing only - resets the siteID to 0 so that it can be set again + resetSiteID: function () { + siteID = 0; + }, + /** * Make a server request from the list of BidRequests. * - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest A object contains bids and other info like gdprConsent. * @return {object} Info describing the request to the server. */ @@ -1717,7 +1851,7 @@ export const spec = { for (const type in adUnitMediaTypes) { switch (adUnitMediaTypes[type]) { case BANNER: - createBannerImps(validBidRequest, missingBannerSizes, bannerImps); + createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest); break; case VIDEO: createVideoImps(validBidRequest, videoImps) @@ -1786,19 +1920,24 @@ export const spec = { * * @param {object} serverResponse A successful response from the server. * @param {object} bidderRequest The bid request sent to the server. - * @return {array} An array of bids which were nested inside the server. + * @return {Array} An array of bids which were nested inside the server. */ interpretResponse: function (serverResponse, bidderRequest) { const bids = []; let bid = null; - if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { - FEATURE_TOGGLES.setFeatureToggles(serverResponse); + // Extract the FLEDGE auction configuration list from the response + let fledgeAuctionConfigs = deepAccess(serverResponse, 'body.ext.protectedAudienceAuctionConfigs') || []; + + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + + if (!serverResponse.hasOwnProperty('body')) { return bids; } const responseBody = serverResponse.body; - const seatbid = responseBody.seatbid; + const seatbid = responseBody.seatbid || []; + for (let i = 0; i < seatbid.length; i++) { if (!seatbid[i].hasOwnProperty('bid')) { continue; @@ -1834,8 +1973,28 @@ export const spec = { } } - FEATURE_TOGGLES.setFeatureToggles(serverResponse); - return bids; + if (Array.isArray(fledgeAuctionConfigs) && fledgeAuctionConfigs.length > 0) { + // Validate and filter fledgeAuctionConfigs + fledgeAuctionConfigs = fledgeAuctionConfigs.filter(config => { + if (!isValidAuctionConfig(config)) { + logWarn('Malformed auction config detected:', config); + return false; + } + return true; + }); + + try { + return { + bids, + fledgeAuctionConfigs, + }; + } catch (error) { + logWarn('Error attaching AuctionConfigs', error); + return bids; + } + } else { + return bids; + } }, /** @@ -1853,8 +2012,8 @@ export const spec = { /** * Determine which user syncs should occur * @param {object} syncOptions - * @param {array} serverResponses - * @returns {array} User sync pixels + * @param {Array} serverResponses + * @returns {Array} User sync pixels */ getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; @@ -1895,11 +2054,11 @@ export const spec = { }; /** - * Build img user sync url - * @param {int} syncsPerBidder number of syncs Per Bidder - * @param {int} index index to pass - * @returns {string} img user sync url - */ + * Build img user sync url + * @param {number} syncsPerBidder number of syncs Per Bidder + * @param {number} index index to pass + * @returns {string} img user sync url + */ function buildImgSyncUrl(syncsPerBidder, index) { let consentString = ''; let gdprApplies = '0'; @@ -1909,13 +2068,14 @@ function buildImgSyncUrl(syncsPerBidder, index) { if (gdprConsent && gdprConsent.hasOwnProperty('consentString')) { consentString = gdprConsent.consentString || ''; } + let siteIdParam = siteID !== 0 ? '&site_id=' + siteID.toString() : ''; - return IMG_USER_SYNC_URL + '&site_id=' + siteID.toString() + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); + return IMG_USER_SYNC_URL + siteIdParam + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); } /** * Combines all imps into a single object - * @param {array} imps array of imps + * @param {Array} imps array of imps * @returns object */ export function combineImps(imps) { @@ -2056,4 +2216,28 @@ function getFormatCount(imp) { return formatCount; } +/** + * Checks if auction config is valid + * @param {object} config + * @returns bool + */ +function isValidAuctionConfig(config) { + return typeof config === 'object' && config !== null; +} + +/** + * Adds device.w / device.h info + * @param {object} r + * @returns object + */ +export function addDeviceInfo(r) { + if (r.device == undefined) { + r.device = {}; + } + r.device.h = window.screen.height; + r.device.w = window.screen.width; + + return r; +} + registerBidder(spec); diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 638cb11c5ab..0705c5932cf 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -469,6 +469,11 @@ pbjs.setConfig({ The timeout value must be a positive whole number in milliseconds. +Protected Audience API (FLEDGE) +=========================== + +In order to enable receiving [Protected Audience API](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) traffic, follow Prebid's documentation on [fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) module to build and enable Fledge. + Additional Information ====================== diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index b587011c748..1e07ce6b5d8 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getDNT, isArray, logWarn} from '../src/utils.js'; +import {deepAccess, getDNT, isArray, logWarn, isFn, isPlainObject, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -7,13 +7,36 @@ import {ajax} from '../src/ajax.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; +const ADAPTER_VERSION = '2.1.0'; +const PREBID_VERSION = '$prebid.version$'; + const BIDDER_CODE = 'jixie'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); -const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.media/jxhbrenderer.1.1.min.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; +/** + * Get bid floor from Price Floors Module + * + * @param {Object} bid + * @returns {float||null} + */ +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + /** * Own miscellaneous support functions: */ @@ -38,12 +61,24 @@ function setIds_(clientId, sessionId) { } catch (error) {} } -function fetchIds_() { +/** + * fetch some ids from cookie, LS. + * @returns + */ +const defaultGenIds_ = [ + { id: '_jxtoko' }, + { id: '_jxifo' }, + { id: '_jxtdid' }, + { id: '_jxcomp' } +]; + +function fetchIds_(cfg) { let ret = { client_id_c: '', client_id_ls: '', session_id_c: '', - session_id_ls: '' + session_id_ls: '', + jxeids: {} }; try { let tmp = storage.getCookie('_jxx'); @@ -55,8 +90,12 @@ function fetchIds_() { if (tmp) ret.client_id_ls = tmp; tmp = storage.getDataFromLocalStorage('_jxxs'); if (tmp) ret.session_id_ls = tmp; - tmp = storage.getCookie('_jxtoko'); - if (tmp) ret.jxtoko_id = tmp; + + let arr = cfg.genids ? cfg.genids : defaultGenIds_; + arr.forEach(function(o) { + tmp = storage.getCookie(o.ck ? o.ck : o.id); + if (tmp) ret.jxeids[o.id] = tmp; + }); } catch (error) {} return ret; } @@ -73,14 +112,6 @@ function getDevice_() { return device; } -function pingTracking_(endpointOverride, qpobj) { - internal.ajax((endpointOverride || EVENTS_URL), null, qpobj, { - withCredentials: true, - method: 'GET', - crossOrigin: true - }); -} - function jxOutstreamRender_(bidAd) { bidAd.renderer.push(() => { window.JixieOutstreamVideo.init({ @@ -132,17 +163,6 @@ function getMiscDims_() { return ret; } -/* function addUserId(eids, id, source, rti) { - if (id) { - if (rti) { - eids.push({ source, id, rti_partner: rti }); - } else { - eids.push({ source, id }); - } - } - return eids; -} */ - // easier for replacement in the unit test export const internal = { getDevice: getDevice_, @@ -153,7 +173,6 @@ export const internal = { export const spec = { code: BIDDER_CODE, - EVENTS_URL: EVENTS_URL, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -170,39 +189,38 @@ export const spec = { let bids = []; validBidRequests.forEach(function(one) { - bids.push({ + let gpid = deepAccess(one, 'ortb2Imp.ext.gpid', deepAccess(one, 'ortb2Imp.ext.data.pbadslot', '')); + let tmp = { bidId: one.bidId, adUnitCode: one.adUnitCode, mediaTypes: (one.mediaTypes === 'undefined' ? {} : one.mediaTypes), sizes: (one.sizes === 'undefined' ? [] : one.sizes), params: one.params, - }); + gpid: gpid + }; + let bidFloor = getBidFloor(one); + if (bidFloor) { + tmp.bidFloor = bidFloor; + } + bids.push(tmp); }); + let jxCfg = config.getConfig('jixie') || {}; - let jixieCfgBlob = config.getConfig('jixie'); - if (!jixieCfgBlob) { - jixieCfgBlob = {}; - } - - let ids = fetchIds_(); + let ids = fetchIds_(jxCfg); let eids = []; let miscDims = internal.getMiscDims(); let schain = deepAccess(validBidRequests[0], 'schain'); - let eids1 = validBidRequests[0].userIdAsEids + let eids1 = validBidRequests[0].userIdAsEids; // all available user ids are sent to our backend in the standard array layout: if (eids1 && eids1.length) { eids = eids1; } // we want to send this blob of info to our backend: - let pg = config.getConfig('priceGranularity'); - if (!pg) { - pg = {}; - } - let transformedParams = Object.assign({}, { // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionid: bidderRequest.auctionId, + auctionid: bidderRequest.auctionId || '', + aid: jxCfg.aid || '', timeout: bidderRequest.timeout, currency: currency, timestamp: (new Date()).getTime(), @@ -213,8 +231,10 @@ export const spec = { bids: bids, eids: eids, schain: schain, - pricegranularity: pg, - cfg: jixieCfgBlob + pricegranularity: (config.getConfig('priceGranularity') || {}), + ver: ADAPTER_VERSION, + pbjsver: PREBID_VERSION, + cfg: jxCfg }, ids); return Object.assign({}, { method: 'POST', @@ -225,48 +245,20 @@ export const spec = { }, onTimeout: function(timeoutData) { - let jxCfgBlob = config.getConfig('jixie'); - if (jxCfgBlob && jxCfgBlob.onTimeout == 'off') { - return; - } - let url = null;// default - if (jxCfgBlob && jxCfgBlob.onTimeoutUrl && typeof jxCfgBlob.onTimeoutUrl == 'string') { - url = jxCfgBlob.onTimeoutUrl; - } - let miscDims = internal.getMiscDims(); - pingTracking_(url, // no overriding ping URL . just use default - { - action: 'hbtimeout', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - auctionid: deepAccess(timeoutData, '0.auctionId'), - timeout: deepAccess(timeoutData, '0.timeout'), - count: timeoutData.length - }); + logError('jixie adapter timed out for the auction.', timeoutData); }, onBidWon: function(bid) { - if (bid.notrack) { - return; - } if (bid.trackingUrl) { - pingTracking_(bid.trackingUrl, {}); - } else { - let miscDims = internal.getMiscDims(); - pingTracking_((bid.trackingUrlBase ? bid.trackingUrlBase : null), { - action: 'hbbidwon', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - cid: bid.cid, - cpid: bid.cpid, - jxbidid: bid.jxBidId, - auctionid: bid.auctionId, - cpm: bid.cpm, - requestid: bid.requestId + internal.ajax(bid.trackingUrl, null, {}, { + withCredentials: true, + method: 'GET', + crossOrigin: true }); } + logInfo( + `jixie adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}` + ); }, interpretResponse: function(response, bidRequest) { @@ -274,7 +266,6 @@ export const spec = { const bidResponses = []; response.body.bids.forEach(function(oneBid) { let bnd = {}; - Object.assign(bnd, oneBid); if (oneBid.osplayer) { bnd.adResponse = { @@ -304,6 +295,21 @@ export const spec = { } return bidResponses; } else { return []; } + }, + + getUserSyncs: function(syncOptions, serverResponses) { + if (!serverResponses.length || !serverResponses[0].body || !serverResponses[0].body.userSyncs) { + return false; + } + let syncs = []; + serverResponses[0].body.userSyncs.forEach(function(sync) { + if (syncOptions.iframeEnabled) { + syncs.push(sync.uf ? { url: sync.uf, type: 'iframe' } : { url: sync.up, type: 'image' }); + } else if (syncOptions.pixelEnabled && sync.up) { + syncs.push({url: sync.up, type: 'image'}) + } + }) + return syncs; } } diff --git a/modules/justIdSystem.js b/modules/justIdSystem.js index 26e9275bbab..a5698023020 100644 --- a/modules/justIdSystem.js +++ b/modules/justIdSystem.js @@ -10,6 +10,13 @@ import { submodule } from '../src/hook.js' import { loadExternalScript } from '../src/adloader.js' import {includes} from '../src/polyfill.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'justId'; const EXTERNAL_SCRIPT_MODULE_CODE = 'justtag'; const LOG_PREFIX = 'User ID - JustId submodule: '; diff --git a/modules/jwplayerBidAdapter.js b/modules/jwplayerBidAdapter.js new file mode 100644 index 00000000000..151d08bf3a6 --- /dev/null +++ b/modules/jwplayerBidAdapter.js @@ -0,0 +1,412 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO } from '../src/mediaTypes.js'; +import { isArray, isFn, deepAccess, deepSetValue, getDNT, logError, logWarn } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; + +const BIDDER_CODE = 'jwplayer'; +const BASE_URL = 'https://vpb-server.jwplayer.com/'; +const AUCTION_URL = BASE_URL + 'openrtb2/auction'; +const USER_SYNC_URL = BASE_URL + 'setuid'; +const GVLID = 1046; +const SUPPORTED_AD_TYPES = [VIDEO]; + +const VIDEO_ORTB_PARAMS = [ + 'pos', + 'w', + 'h', + 'playbackend', + 'mimes', + 'minduration', + 'maxduration', + 'protocols', + 'startdelay', + 'placement', + 'plcmt', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; + +function getBidAdapter() { + function isBidRequestValid(bid) { + const params = bid && bid.params; + if (!params) { + return false; + } + + return !!params.placementId && !!params.publisherId && !!params.siteId; + } + + function buildRequests(bidRequests, bidderRequest) { + if (!bidRequests) { + return; + } + + if (!hasContentUrl(bidderRequest.ortb2)) { + logError(`${BIDDER_CODE}: cannot bid without a valid Content URL. Please populate ortb2.site.content.url`); + return; + } + + const warnings = getWarnings(bidderRequest); + warnings.forEach(warning => { + logWarn(`${BIDDER_CODE}: ${warning}`); + }); + + return bidRequests.map(bidRequest => { + const payload = buildRequest(bidRequest, bidderRequest); + + return { + method: 'POST', + url: AUCTION_URL, + data: payload + } + }); + } + + function interpretResponse(serverResponse) { + const outgoingBidResponses = []; + const serverResponseBody = serverResponse.body; + + logResponseWarnings(serverResponseBody); + + const seatBids = serverResponseBody && serverResponseBody.seatbid; + if (!isArray(seatBids)) { + return outgoingBidResponses; + } + + const cur = serverResponseBody.cur; + + seatBids.forEach(seatBid => { + seatBid.bid.forEach(bid => { + const bidResponse = { + requestId: serverResponseBody.id, + cpm: bid.price, + currency: cur, + width: bid.w, + height: bid.h, + ad: bid.adm, + vastXml: bid.adm, + ttl: bid.ttl || 3600, + netRevenue: false, + creativeId: bid.adid, + dealId: bid.dealid, + meta: { + advertiserDomains: bid.adomain, + mediaType: VIDEO, + primaryCatId: bid.cat, + } + }; + + outgoingBidResponses.push(bidResponse); + }); + }); + + return outgoingBidResponses; + } + + function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (!hasPurpose1Consent(gdprConsent)) { + return []; + } + + const userSyncs = []; + const consentQueryParams = getUserSyncConsentQueryParams(gdprConsent); + const url = `https://ib.adnxs.com/getuid?${USER_SYNC_URL}?bidder=jwplayer&uid=$UID&f=i` + consentQueryParams + + if (syncOptions.iframeEnabled) { + userSyncs.push({ + type: 'iframe', + url + }); + } + + if (syncOptions.pixelEnabled) { + userSyncs.push({ + type: 'image', + url + }); + } + + return userSyncs; + } + + return { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs + } + + function getUserSyncConsentQueryParams(gdprConsent) { + if (!gdprConsent) { + return ''; + } + + const consentString = gdprConsent.consentString; + if (!consentString) { + return ''; + } + + let gdpr = 0; + const gdprApplies = gdprConsent.gdprApplies; + if (typeof gdprApplies === 'boolean') { + gdpr = Number(gdprApplies) + } + + return `&gdpr=${gdpr}&gdpr_consent=${consentString}`; + } + + function buildRequest(bidRequest, bidderRequest) { + const openrtbRequest = { + id: bidRequest.bidId, + imp: getRequestImpressions(bidRequest, bidderRequest), + site: getRequestSite(bidRequest, bidderRequest), + device: getRequestDevice(bidderRequest.ortb2), + user: getRequestUser(bidderRequest.ortb2), + }; + + // GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + if (bidRequest.schain) { + deepSetValue(openrtbRequest, 'source.schain', bidRequest.schain); + } + + openrtbRequest.tmax = bidderRequest.timeout || 200; + + return JSON.stringify(openrtbRequest); + } + + function getRequestImpressions(bidRequest) { + const impressionObject = { + id: bidRequest.adUnitCode, + }; + + impressionObject.video = getImpressionVideo(bidRequest); + + const bidFloorData = getBidFloorData(bidRequest); + if (bidFloorData) { + impressionObject.bidfloor = bidFloorData.floor; + impressionObject.bidfloorcur = bidFloorData.currency; + } + + impressionObject.ext = getImpressionExtension(bidRequest); + + return [impressionObject]; + } + + function getImpressionVideo(bidRequest) { + const videoParams = deepAccess(bidRequest, 'mediaTypes.video', {}); + + const video = {}; + + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + setPlayerSize(video, videoParams); + + if (!videoParams.plcmt) { + logWarn(`${BIDDER_CODE}: Please set a value to mediaTypes.video.plcmt`); + } + + return video; + } + + function getImpressionExtension(bidRequest) { + return { + prebid: { + bidder: { + jwplayer: { + placementId: bidRequest.params.placementId + } + } + } + }; + } + + function setPlayerSize(videoImp, videoParams) { + if (videoImp.w !== undefined && videoImp.h !== undefined) { + return; + } + + const playerSize = getNormalizedPlayerSize(videoParams.playerSize); + if (!playerSize.length) { + logWarn(logWarn(`${BIDDER_CODE}: Video size has not been set. Please set values in video.h and video.w`)); + return; + } + + if (videoImp.w === undefined) { + videoImp.w = playerSize[0]; + } + + if (videoImp.h === undefined) { + videoImp.h = playerSize[1]; + } + } + + function getNormalizedPlayerSize(playerSize) { + if (!Array.isArray(playerSize)) { + return []; + } + + if (Array.isArray(playerSize[0])) { + playerSize = playerSize[0]; + } + + if (playerSize.length < 2) { + return []; + } + + return playerSize; + } + + function getBidFloorData(bidRequest) { + const { params } = bidRequest; + const currency = params.currency || 'USD'; + + let floorData; + if (isFn(bidRequest.getFloor)) { + const bidFloorRequest = { + currency: currency, + mediaType: VIDEO, + size: '*' + }; + floorData = bidRequest.getFloor(bidFloorRequest); + } else if (params.bidFloor) { + floorData = { floor: params.bidFloor, currency: currency }; + } + + return floorData; + } + + function getRequestSite(bidRequest, bidderRequest) { + const site = bidderRequest.ortb2.site || {}; + + site.domain = site.domain || config.publisherDomain || window.location.hostname; + site.page = site.page || config.pageUrl || window.location.href; + + const referer = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + if (!site.ref && referer) { + site.ref = referer; + } + + const jwplayerPublisherExtChain = 'publisher.ext.jwplayer.'; + + deepSetValue(site, jwplayerPublisherExtChain + 'publisherId', bidRequest.params.publisherId); + deepSetValue(site, jwplayerPublisherExtChain + 'siteId', bidRequest.params.siteId); + + return site; + } + + function getRequestDevice(ortb2) { + const device = Object.assign({ + h: screen.height, + w: screen.width, + ua: navigator.userAgent, + dnt: getDNT() ? 1 : 0, + js: 1 + }, ortb2.device || {}) + + const language = getLanguage(); + if (!device.language && language) { + device.language = language; + } + + return device; + } + + function getLanguage() { + const navigatorLanguage = navigator.language; + if (!navigatorLanguage) { + return; + } + + const languageCodeSegments = navigatorLanguage.split('-'); + if (!languageCodeSegments.length) { + return; + } + + return languageCodeSegments[0]; + } + + function getRequestUser(ortb2) { + const user = ortb2.user || {}; + if (config.getConfig('coppa') === true) { + user.coppa = true; + } + + return user; + } + + function hasContentUrl(ortb2) { + const site = ortb2.site; + const content = site && site.content; + return !!(content && content.url); + } + + function getWarnings(bidderRequest) { + const content = bidderRequest.ortb2.site.content; + const contentChain = 'ortb2.site.content.'; + const warnings = []; + if (!content.id) { + warnings.push(getMissingFieldMessage(contentChain + 'id')); + } + + if (!content.title) { + warnings.push(getMissingFieldMessage(contentChain + 'title')); + } + + if (!content.ext || !content.ext.description) { + warnings.push(getMissingFieldMessage(contentChain + 'ext.description')); + } + + return warnings; + } + + function getMissingFieldMessage(fieldName) { + return `Optional field ${fieldName} is not populated; we recommend populating for maximum performance.` + } + + function logResponseWarnings(serverResponseBody) { + const warningPayload = deepAccess(serverResponseBody, 'ext.warnings'); + if (!warningPayload) { + return; + } + + const warningCategories = Object.keys(warningPayload); + warningCategories.forEach(category => { + const warnings = warningPayload[category]; + if (!isArray(warnings)) { + return; + } + + warnings.forEach(warning => { + logWarn(`${BIDDER_CODE}: [Bid Response][Warning Code: ${warning.code}] ${warning.message}`); + }); + }); + } +} + +export const spec = getBidAdapter(); + +registerBidder(spec); diff --git a/modules/jwplayerBidAdapter.md b/modules/jwplayerBidAdapter.md new file mode 100644 index 00000000000..620f8657e50 --- /dev/null +++ b/modules/jwplayerBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: JWPlayer Bid Adapter +Module Type: Bidder Adapter +Maintainer: boost-engineering@jwplayer.com +``` + +# Description + +Connects to JWPlayer's demand sources. + +JWPlayer bid adapter supports Video (instream and outstream). + +# Sample Ad Unit + +```markdown +const adUnit = { + code: 'test-ad-unit', + mediaTypes: { + video: { + pos: 0, + w: 640, + h: 480, + mimes : ['video/x-ms-wmv', 'video/mp4'], + minduration : 0, + maxduration: 60, + protocols : [2,3,7,5,6,8], + startdelay: 0, + placement: 1, + plcmt: 1, + skip: 1, + skipafter: 10, + playbackmethod: [3], + api: [2], + linearity: 1 + } + }, + bids: [{ + bidder: 'jwplayer', + params: { + publisherId: 'test-publisher-id', + siteId: 'test-site-id', + placementId: 'test-placement-id' + } + }] +}; +``` + +# Sample ortb2 config + +```markdown +pbjs.setConfig({ + ortb2: { + site: { + publisher: { + id: 'test-publisher-id' + }, + content: { + id: 'test-media-id', + url: 'test.mp4', + title: 'title of my media', + ext: { + description: 'description of my media' + } + }, + domain: 'test-domain.com', + page: 'https://www.test-domain.com/test.html', + } + } +} +``` diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index b79843dccfd..29ce0da5317 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -16,26 +16,41 @@ import {deepAccess, logError} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').adUnit} adUnit + */ + const SUBMODULE_NAME = 'jwplayer'; const JWPLAYER_DOMAIN = SUBMODULE_NAME + '.com'; -const segCache = {}; +const ENRICH_ALWAYS = 'always'; +const ENRICH_WHEN_EMPTY = 'whenEmpty'; +const ENRICH_NEVER = 'never'; +const overrideValidationRegex = /^(always|never|whenEmpty)$/; +const playlistItemCache = {}; const pendingRequests = {}; let activeRequestCount = 0; let resumeBidRequest; +// defaults to 'always' for backwards compatibility +// TODO: Prebid 9 - replace with ENRICH_WHEN_EMPTY +let overrideContentId = ENRICH_ALWAYS; +let overrideContentUrl = ENRICH_WHEN_EMPTY; +let overrideContentTitle = ENRICH_WHEN_EMPTY; +let overrideContentDescription = ENRICH_WHEN_EMPTY; /** @type {RtdSubmodule} */ export const jwplayerSubmodule = { /** - * used to link submodule with realTimeData - * @type {string} - */ + * used to link submodule with realTimeData + * @type {string} + */ name: SUBMODULE_NAME, /** - * add targeting data to bids and signal completion to realTimeData module - * @function - * @param {Obj} bidReqConfig - * @param {function} onDone - */ + * add targeting data to bids and signal completion to realTimeData module + * @function + * @param {object} bidReqConfig + * @param {function} onDone + */ getBidRequestData: enrichBidRequest, init }; @@ -48,6 +63,7 @@ config.getConfig('realTimeData', ({realTimeData}) => { return; } fetchTargetingInformation(params); + setOverrides(params); }); submodule('realTimeData', jwplayerSubmodule); @@ -66,15 +82,32 @@ export function fetchTargetingInformation(jwTargeting) { }); } +export function setOverrides(params) { + // For backwards compatibility, default to always unless overridden by Publisher. + // TODO: Prebid 9 - replace with ENRICH_WHEN_EMPTY + overrideContentId = sanitizeOverrideParam(params.overrideContentId, ENRICH_ALWAYS); + overrideContentUrl = sanitizeOverrideParam(params.overrideContentUrl, ENRICH_WHEN_EMPTY); + overrideContentTitle = sanitizeOverrideParam(params.overrideContentTitle, ENRICH_WHEN_EMPTY); + overrideContentDescription = sanitizeOverrideParam(params.overrideContentDescription, ENRICH_WHEN_EMPTY); +} + +function sanitizeOverrideParam(overrideParam, defaultValue) { + if (overrideValidationRegex.test(overrideParam)) { + return overrideParam; + } + + return defaultValue; +} + export function fetchTargetingForMediaId(mediaId) { const ajax = ajaxBuilder(); // TODO: Avoid checking undefined vs null by setting a callback to pendingRequests. pendingRequests[mediaId] = null; ajax(`https://cdn.${JWPLAYER_DOMAIN}/v2/media/${mediaId}`, { success: function (response) { - const segment = parseSegment(response); - cacheSegments(segment, mediaId); - onRequestCompleted(mediaId, !!segment); + const item = parsePlaylistItem(response); + cachePlaylistItem(item, mediaId); + onRequestCompleted(mediaId, !!item); }, error: function () { logError('failed to retrieve targeting information'); @@ -83,8 +116,8 @@ export function fetchTargetingForMediaId(mediaId) { }); } -function parseSegment(response) { - let segment; +function parsePlaylistItem(response) { + let item; try { const data = JSON.parse(response); if (!data) { @@ -96,16 +129,16 @@ function parseSegment(response) { throw ('Empty playlist'); } - segment = playlist[0].jwpseg; + item = playlist[0]; } catch (err) { logError(err); } - return segment; + return item; } -function cacheSegments(jwpseg, mediaId) { - if (jwpseg && mediaId) { - segCache[mediaId] = jwpseg; +function cachePlaylistItem(playlistItem, mediaId) { + if (playlistItem && mediaId) { + playlistItemCache[mediaId] = playlistItem; } } @@ -162,7 +195,7 @@ export function enrichAdUnits(adUnits, ortb2Fragments = {}) { const contentData = getContentData(mediaId, contentSegments); const targeting = formatTargetingResponse(vat); enrichBids(adUnit.bids, targeting, contentId, contentData); - addOrtbSiteContent(ortb2Fragments.global, contentId, contentData); + addOrtbSiteContent(ortb2Fragments.global, contentId, contentData, vat.title, vat.description, vat.mediaUrl); }; loadVat(jwTargeting, onVatResponse); }); @@ -187,18 +220,22 @@ export function extractPublisherParams(adUnit, fallback) { } function loadVat(params, onCompletion) { - const { playerID, mediaID } = params; + let { playerID, playerDivId, mediaID } = params; + if (!playerDivId) { + playerDivId = playerID; + } + if (pendingRequests[mediaID] !== undefined) { - loadVatForPendingRequest(playerID, mediaID, onCompletion); + loadVatForPendingRequest(playerDivId, mediaID, onCompletion); return; } - const vat = getVatFromCache(mediaID) || getVatFromPlayer(playerID, mediaID) || { mediaID }; + const vat = getVatFromCache(mediaID) || getVatFromPlayer(playerDivId, mediaID) || { mediaID }; onCompletion(vat); } -function loadVatForPendingRequest(playerID, mediaID, callback) { - const vat = getVatFromPlayer(playerID, mediaID); +function loadVatForPendingRequest(playerDivId, mediaID, callback) { + const vat = getVatFromPlayer(playerDivId, mediaID); if (vat) { callback(vat); } else { @@ -208,20 +245,29 @@ function loadVatForPendingRequest(playerID, mediaID, callback) { } export function getVatFromCache(mediaID) { - const segments = segCache[mediaID]; + const item = playlistItemCache[mediaID]; - if (!segments) { + if (!item) { return null; } + const mediaUrl = item.file ?? getFileFromSources(item); + return { - segments, + segments: item.jwpseg, + title: item.title, + description: item.description, + mediaUrl, mediaID }; } -export function getVatFromPlayer(playerID, mediaID) { - const player = getPlayer(playerID); +function getFileFromSources(playlistItem) { + return playlistItem.sources?.find?.(source => !!source.file)?.file; +} + +export function getVatFromPlayer(playerDivId, mediaID) { + const player = getPlayer(playerDivId); if (!player) { return null; } @@ -232,12 +278,18 @@ export function getVatFromPlayer(playerID, mediaID) { } mediaID = mediaID || item.mediaid; + const title = item.title; + const description = item.description; + const mediaUrl = item.file; const segments = item.jwpseg; - cacheSegments(segments, mediaID) + cachePlaylistItem(item, mediaID) return { segments, - mediaID + mediaID, + title, + mediaUrl, + description }; } @@ -304,11 +356,7 @@ export function getContentData(mediaId, segments) { return contentData; } -export function addOrtbSiteContent(ortb2, contentId, contentData) { - if (!contentId && !contentData) { - return; - } - +export function addOrtbSiteContent(ortb2, contentId, contentData, contentTitle, contentDescription, contentUrl) { if (ortb2 == null) { ortb2 = {}; } @@ -316,11 +364,24 @@ export function addOrtbSiteContent(ortb2, contentId, contentData) { let site = ortb2.site = ortb2.site || {}; let content = site.content = site.content || {}; - if (contentId) { + if (shouldOverride(content.id, contentId, overrideContentId)) { content.id = contentId; } - const currentData = content.data = content.data || []; + if (shouldOverride(content.url, contentUrl, overrideContentUrl)) { + content.url = contentUrl; + } + + if (shouldOverride(content.title, contentTitle, overrideContentTitle)) { + content.title = contentTitle; + } + + if (shouldOverride(content.ext && content.ext.description, contentDescription, overrideContentDescription)) { + content.ext = content.ext || {}; + content.ext.description = contentDescription; + } + + const currentData = content.data || []; // remove old jwplayer data const data = currentData.filter(datum => datum.name !== JWPLAYER_DOMAIN); @@ -328,11 +389,26 @@ export function addOrtbSiteContent(ortb2, contentId, contentData) { data.push(contentData); } - content.data = data; + if (data.length) { + content.data = data; + } return ortb2; } +function shouldOverride(currentValue, newValue, configValue) { + switch (configValue) { + case ENRICH_ALWAYS: + return !!newValue; + case ENRICH_NEVER: + return false; + case ENRICH_WHEN_EMPTY: + return !!newValue && currentValue === undefined; + default: + return false; + } +} + function enrichBids(bids, targeting, contentId, contentData) { if (!bids) { return; @@ -357,14 +433,14 @@ export function addTargetingToBid(bid, targeting) { bid.rtd = Object.assign({}, rtd, jwRtd); } -function getPlayer(playerID) { +function getPlayer(playerDivId) { const jwplayer = window.jwplayer; if (!jwplayer) { logError(SUBMODULE_NAME + '.js was not found on page'); return; } - const player = jwplayer(playerID); + const player = jwplayer(playerDivId); if (!player || !player.getPlaylist) { logError('player ID did not match any players'); return; diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 479829196ed..936cd1d10a2 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -36,7 +36,7 @@ const adUnit = { data: { jwTargeting: { // Note: the following Ids are placeholders and should be replaced with your Ids. - playerID: 'abcd', + playerDivId: 'abcd', mediaID: '1234' } } @@ -51,7 +51,7 @@ pbjs.que.push(function() { }); }); ``` -**Note**: The player ID is the ID of the HTML div element used when instantiating the player. +**Note**: The player Div ID is the ID of the HTML div element used when instantiating the player. You can retrieve this ID by calling `player.id`, where player is the JW Player instance variable. **Note**: You may also include `jwTargeting` information in the prebid config's `ortb2.site.ext.data`. Information provided in the adUnit will always supersede, and information in the config will be used as a fallback. @@ -78,6 +78,19 @@ realTimeData = { }; ``` +## Configuration syntax + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| name | String | Real time data module name | Always 'jwplayer' | +| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false | +| params | Object | | | +| params.mediaIDs | Array of Strings | Media Ids for prefetching | Optional | +| params.overrideContentId | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.id | Defaults to 'always' | +| params.overrideContentUrl | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.url | Defaults to 'whenEmpty' | +| params.overrideContentTitle | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.title | Defaults to 'whenEmpty' | +| params.overrideContentDescription | String enum: 'always', 'whenEmpty' or 'never' | Determines when the module should update the oRTB site.content.ext.description | Defaults to 'whenEmpty' | + # Usage for Bid Adapters: Implement the `buildRequests` function. When it is called, the `bidRequests` param will be an array of bids. @@ -94,6 +107,8 @@ Example: site: { content: { id: 'jw_abc123', + title: 'media title', + url: 'https:www.cdn.com/media.mp4', data: [{ name: 'jwplayer.com', ext: { @@ -105,7 +120,10 @@ Example: }, { id: '456' }] - }] + }], + ext: { + description: 'media description' + } } } } @@ -116,7 +134,10 @@ where: - `ortb2` is an object containing first party data - `site` is an object containing page specific information - `content` is an object containing metadata for the media. It may contain the following information: - - `id` is a unique identifier for the specific media asset + - `id` is a unique identifier for the specific media asset, + - `title` is the title of the media content + - `url` is the url of the media asset + - `ext.description` is the description of the media content - `data` is an array containing segment taxonomy objects that have the following parameters: - `name` is the `jwplayer.com` string indicating the provider name - `ext.segtax` whose `502` value is the unique identifier for JW Player's proprietary taxonomy diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 1dde4453222..fe22915223e 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -1,4 +1,4 @@ -import { _each, isEmpty, buildUrl, deepAccess, pick, triggerPixel } from '../src/utils.js'; +import { _each, isEmpty, buildUrl, deepAccess, pick, triggerPixel, logError } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -24,6 +24,7 @@ const CURRENCY = Object.freeze({ }); const REQUEST_KEYS = Object.freeze({ + USER_DATA: 'ortb2.user.data', SOCIAL_CANVAS: 'params.socialCanvas', SUA: 'ortb2.device.sua', TDID_ADAPTER: 'userId.tdid', @@ -94,18 +95,32 @@ function buildRequests(validBidRequests, bidderRequest) { ] }, imp: impressions, - user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent), + user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent) }); + // Add full ortb2 object as backup + if (firstBidRequest.ortb2) { + const siteCat = firstBidRequest.ortb2.site?.cat; + if (siteCat != null) { + krakenParams.site = { cat: siteCat }; + } + krakenParams.ext = { ortb2: firstBidRequest.ortb2 }; + } + + // Add schain if (firstBidRequest.schain && firstBidRequest.schain.nodes) { krakenParams.schain = firstBidRequest.schain } + // Add user data object if available + krakenParams.user.data = deepAccess(firstBidRequest, REQUEST_KEYS.USER_DATA) || []; + const reqCount = getRequestCount() if (reqCount != null) { krakenParams.requestCount = reqCount; } + // Add currency if not USD if (currency != null && currency != CURRENCY.US_DOLLAR) { krakenParams.cur = currency; } @@ -209,7 +224,7 @@ function interpretResponse(response, bidRequest) { width: adUnit.width, height: adUnit.height, ttl: 300, - creativeId: adUnit.id, + creativeId: adUnit.creativeID, dealId: adUnit.targetingCustom, netRevenue: true, currency: adUnit.currency || bidRequest.currency, @@ -344,56 +359,57 @@ function getUserIds(tdidAdapter, usp, gdpr, eids, gpp) { crbIDs: crb.syncIds || {} }; - // Pull Trade Desk ID from adapter - if (tdidAdapter) { - userIds.tdID = tdidAdapter; - } - - // Pull Trade Desk ID from our storage + // Pull Trade Desk ID if (!tdidAdapter && crb.tdID) { userIds.tdID = crb.tdID; + } else if (tdidAdapter) { + userIds.tdID = tdidAdapter; } + // USP if (usp) { userIds.usp = usp; } - try { - if (gdpr) { - userIds['gdpr'] = { - consent: gdpr.consentString || '', - applies: !!gdpr.gdprApplies, - } - } - } catch (e) { + // GDPR + if (gdpr) { + userIds.gdpr = { + consent: gdpr.consentString || '', + applies: !!gdpr.gdprApplies, + }; } + // Kargo ID if (crb.lexId != null) { userIds.kargoID = crb.lexId; } + // Client ID if (crb.clientId != null) { userIds.clientID = crb.clientId; } + // Opt Out if (crb.optOut != null) { userIds.optOut = crb.optOut; } + // User ID Sub-Modules (userIdAsEids) if (eids != null) { userIds.sharedIDEids = eids; } + // GPP if (gpp) { - const parsedGPP = {} - if (gpp && gpp.consentString) { - parsedGPP.gppString = gpp.consentString + const parsedGPP = {}; + if (gpp.consentString) { + parsedGPP.gppString = gpp.consentString; } - if (gpp && gpp.applicableSections) { - parsedGPP.applicableSections = gpp.applicableSections + if (gpp.applicableSections) { + parsedGPP.applicableSections = gpp.applicableSections; } if (!isEmpty(parsedGPP)) { - userIds.gpp = parsedGPP + userIds.gpp = parsedGPP; } } @@ -447,10 +463,6 @@ function getImpression(bid) { code: bid.adUnitCode }; - if (bid.floorData != null && bid.floorData.floorMin > 0) { - imp.floor = bid.floorData.floorMin; - } - if (bid.bidRequestsCount > 0) { imp.bidRequestCount = bid.bidRequestsCount; } @@ -463,51 +475,49 @@ function getImpression(bid) { imp.bidderWinCount = bid.bidderWinsCount; } - const gpid = getGPID(bid) - if (gpid != null && gpid != '') { + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { imp.fpd = { gpid: gpid } } - if (bid.mediaTypes != null) { - if (bid.mediaTypes.banner != null) { - imp.banner = bid.mediaTypes.banner; - } + // Add full ortb2Imp object as backup + if (bid.ortb2Imp) { + imp.ext = { ortb2Imp: bid.ortb2Imp }; + } - if (bid.mediaTypes.video != null) { - imp.video = bid.mediaTypes.video; - } + if (bid.mediaTypes) { + const { banner, video, native } = bid.mediaTypes; - if (bid.mediaTypes.native != null) { - imp.native = bid.mediaTypes.native; + if (banner) { + imp.banner = banner; } - } - return imp -} - -function getGPID(bid) { - if (bid.ortb2Imp != null) { - if (bid.ortb2Imp.gpid != null && bid.ortb2Imp.gpid != '') { - return bid.ortb2Imp.gpid; + if (video) { + imp.video = video; } - if (bid.ortb2Imp.ext != null && bid.ortb2Imp.ext.data != null) { - if (bid.ortb2Imp.ext.data.pbAdSlot != null && bid.ortb2Imp.ext.data.pbAdSlot != '') { - return bid.ortb2Imp.ext.data.pbAdSlot; - } + if (native) { + imp.native = native; + } - if (bid.ortb2Imp.ext.data.adServer != null && bid.ortb2Imp.ext.data.adServer.adSlot != null && bid.ortb2Imp.ext.data.adServer.adSlot != '') { - return bid.ortb2Imp.ext.data.adServer.adSlot; + if (typeof bid.getFloor === 'function') { + let floorInfo; + try { + floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + } catch (e) { + logError('Kargo: getFloor threw an error: ', e); } + imp.floor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } } - if (bid.adUnitCode != null && bid.adUnitCode != '') { - return bid.adUnitCode; - } - return ''; + return imp } export const spec = { diff --git a/modules/kimberliteBidAdapter.js b/modules/kimberliteBidAdapter.js new file mode 100644 index 00000000000..72df921e18f --- /dev/null +++ b/modules/kimberliteBidAdapter.js @@ -0,0 +1,71 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { deepSetValue } from '../src/utils.js'; + +const VERSION = '1.0.0'; + +const BIDDER_CODE = 'kimberlite'; +const METHOD = 'POST'; +const ENDPOINT_URL = 'https://kimberlite.io/rtb/bid/pbjs'; + +const VERSION_INFO = { + ver: '$prebid.version$', + adapterVer: `${VERSION}` +}; + +const converter = ortbConverter({ + context: { + mediaType: BANNER, + netRevenue: true, + ttl: 300 + }, + + request(buildRequest, imps, bidderRequest, context) { + const bidRequest = buildRequest(imps, bidderRequest, context); + deepSetValue(bidRequest, 'site.publisher.domain', bidderRequest.refererInfo.domain); + deepSetValue(bidRequest, 'site.page', bidderRequest.refererInfo.page); + deepSetValue(bidRequest, 'ext.prebid.ver', VERSION_INFO.ver); + deepSetValue(bidRequest, 'ext.prebid.adapterVer', VERSION_INFO.adapterVer); + bidRequest.at = 1; + return bidRequest; + }, + + imp (buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.tagid = bidRequest.params.placementId; + return imp; + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bidRequest = {}) => { + const { params, mediaTypes } = bidRequest; + let isValid = Boolean(params && params.placementId); + if (mediaTypes && mediaTypes[BANNER]) { + isValid = isValid && Boolean(mediaTypes[BANNER].sizes); + } else { + isValid = false; + } + + return isValid; + }, + + buildRequests: function (bidRequests, bidderRequest) { + return { + method: METHOD, + url: ENDPOINT_URL, + data: converter.toORTB({ bidderRequest, bidRequests }) + } + }, + + interpretResponse(serverResponse, bidRequest) { + const bids = converter.fromORTB({response: serverResponse.body, request: bidRequest.data}).bids; + return bids; + } +}; + +registerBidder(spec); diff --git a/modules/kimberliteBidAdapter.md b/modules/kimberliteBidAdapter.md new file mode 100644 index 00000000000..c165f1073aa --- /dev/null +++ b/modules/kimberliteBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +```markdown +Module Name: Kimberlite Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@solta.io +``` + +# Description + +Kimberlite exchange adapter. + +# Test Parameters + +## Banner AdUnit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[320, 250], [640, 480]], + } + }, + bids: [ + { + bidder: "kimberlite", + params: { + placementId: 'testBanner' + } + } + ] + } +] +``` diff --git a/modules/kinessoIdSystem.js b/modules/kinessoIdSystem.js index c13ed3976d3..35b8dcc182d 100644 --- a/modules/kinessoIdSystem.js +++ b/modules/kinessoIdSystem.js @@ -10,6 +10,12 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {coppaDataHandler, uspDataHandler} from '../src/adapterManager.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const MODULE_NAME = 'kpuid'; const ID_SVC = 'https://id.knsso.com/id'; // These values should NEVER change. If diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 0a868661310..5a5536e0c1a 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index bb0534d6372..264592cd7d6 100644 --- a/modules/kueezRtbBidAdapter.js +++ b/modules/kueezRtbBidAdapter.js @@ -173,7 +173,7 @@ function appendUserIdsToRequestPayload(payloadRef, userIds) { function buildRequests(validBidRequests, bidderRequest) { const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; - const bidderTimeout = config.getConfig('bidderTimeout'); + const bidderTimeout = bidderRequest.timeout ?? config.getConfig('bidderTimeout'); const requests = []; validBidRequests.forEach(validBidRequest => { const sizes = parseSizesInput(validBidRequest.sizes); @@ -251,13 +251,20 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js deleted file mode 100644 index fb3f6e4e231..00000000000 --- a/modules/kulturemediaBidAdapter.js +++ /dev/null @@ -1,472 +0,0 @@ -import { - deepAccess, - deepSetValue, - isArray, - isFn, - isNumber, - isPlainObject, - isStr, - logError, - logInfo, - logMessage -} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; - -const BIDDER_CODE = 'kulturemedia'; -const DEFAULT_BID_TTL = 300; -const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_NET_REVENUE = true; -const DEFAULT_NETWORK_ID = 1; -const OPENRTB_VIDEO_PARAMS = [ - 'mimes', - 'minduration', - 'maxduration', - 'placement', - 'protocols', - 'startdelay', - 'skip', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackmethod', - 'api', - 'linearity' -]; - -export const spec = { - code: BIDDER_CODE, - VERSION: '1.0.0', - supportedMediaTypes: [BANNER, VIDEO], - ENDPOINT: 'https://ads.kulture.media/pbjs', - - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bidRequest The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return ( - _validateParams(bid) && - _validateBanner(bid) && - _validateVideo(bid) - ); - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests 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 (validBidRequests, bidderRequest) { - if (!validBidRequests || !bidderRequest) { - return; - } - - // We need to refactor this to support mixed content when there are both - // banner and video bid requests - let openrtbRequest; - if (hasBannerMediaType(validBidRequests[0])) { - openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest); - } else if (hasVideoMediaType(validBidRequests[0])) { - openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest); - } - - // adding schain object - if (validBidRequests[0].schain) { - deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); - } - - // Attaching GDPR Consent Params - if (bidderRequest.gdprConsent) { - deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); - } - - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - // EIDS - const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); - if (Array.isArray(eids) && eids.length > 0) { - deepSetValue(openrtbRequest, 'user.ext.eids', eids); - } - - let publisherId = validBidRequests[0].params.publisherId; - let placementId = validBidRequests[0].params.placementId; - const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID; - - if (validBidRequests[0].params.e2etest) { - logMessage('E2E test mode enabled'); - publisherId = 'e2etest' - } - let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; - - if (placementId) { - baseEndpoint += '&placementId=' + placementId - } - if (networkId) { - baseEndpoint += '&nId=' + networkId - } - - const payloadString = JSON.stringify(openrtbRequest); - return { - method: 'POST', - url: baseEndpoint, - data: payloadString, - }; - }, - - interpretResponse: function (serverResponse) { - const bidResponses = []; - const response = (serverResponse || {}).body; - // response is always one seat (exchange) with (optional) bids for each impression - if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { - response.seatbid[0].bid.forEach(bid => { - if (bid.adm && bid.price) { - bidResponses.push(_createBidResponse(bid)); - } - }) - } else { - logInfo('kulturemedia.interpretResponse :: no valid responses to interpret'); - } - return bidResponses; - }, - - getUserSyncs: function (syncOptions, serverResponses) { - logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); - let syncs = []; - - if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { - return syncs; - } - - serverResponses.forEach(resp => { - const userSync = deepAccess(resp, 'body.ext.usersync'); - if (userSync) { - let syncDetails = []; - Object.keys(userSync).forEach(key => { - const value = userSync[key]; - if (value.syncs && value.syncs.length) { - syncDetails = syncDetails.concat(value.syncs); - } - }); - syncDetails.forEach(syncDetails => { - syncs.push({ - type: syncDetails.type === 'iframe' ? 'iframe' : 'image', - url: syncDetails.url - }); - }); - - if (!syncOptions.iframeEnabled) { - syncs = syncs.filter(s => s.type !== 'iframe') - } - if (!syncOptions.pixelEnabled) { - syncs = syncs.filter(s => s.type !== 'image') - } - } - }); - logInfo('kulturemedia.getUserSyncs result=%o', syncs); - return syncs; - }, - -}; - -/* ======================================= - * Util Functions - *======================================= */ - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasBannerMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.banner'); -} - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasVideoMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.video'); -} - -function _validateParams(bidRequest) { - if (!bidRequest.params) { - return false; - } - - if (bidRequest.params.e2etest) { - return true; - } - - if (!bidRequest.params.publisherId) { - logError('Validation failed: publisherId not declared'); - return false; - } - - if (!bidRequest.params.placementId) { - logError('Validation failed: placementId not declared'); - return false; - } - - const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); - if (!mediaTypesExists) { - return false; - } - - return true; -} - -/** - * Validates banner bid request. If it is not banner media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateBanner(bidRequest) { - // If there's no banner no need to validate - if (!hasBannerMediaType(bidRequest)) { - return true; - } - const banner = deepAccess(bidRequest, 'mediaTypes.banner'); - if (!Array.isArray(banner.sizes)) { - return false; - } - - return true; -} - -/** - * Validates video bid request. If it is not video media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateVideo(bidRequest) { - // If there's no video no need to validate - if (!hasVideoMediaType(bidRequest)) { - return true; - } - - const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - const params = deepAccess(bidRequest, 'params', {}); - - if (params && params.e2etest) { - return true; - } - - const videoParams = { - ...videoPlacement, - ...videoBidderParams // Bidder Specific overrides - }; - - if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { - logError('Validation failed: mimes are invalid'); - return false; - } - - if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { - logError('Validation failed: protocols are invalid'); - return false; - } - - if (!videoParams.context) { - logError('Validation failed: context id not declared'); - return false; - } - - if (videoParams.context !== 'instream') { - logError('Validation failed: only context instream is supported '); - return false; - } - - if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { - logError('Validation failed: player size not declared or is not in format [[w,h]]'); - return false; - } - - return true; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildVideoRequestData(bidRequest, bidderRequest) { - const {params} = bidRequest; - - const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - - const videoParams = { - ...videoAdUnit, - ...videoBidderParams // Bidder Specific overrides - }; - - if (bidRequest.params && bidRequest.params.e2etest) { - videoParams.playerSize = [[640, 480]] - videoParams.conext = 'instream' - } - - const video = { - w: parseInt(videoParams.playerSize[0][0], 10), - h: parseInt(videoParams.playerSize[0][1], 10), - } - - // Obtain all ORTB params related video from Ad Unit - OPENRTB_VIDEO_PARAMS.forEach((param) => { - if (videoParams.hasOwnProperty(param)) { - video[param] = videoParams[param]; - } - }); - - // Placement Inference Rules: - // - If no placement is defined then default to 1 (In Stream) - video.placement = video.placement || 2; - - // - If product is instream (for instream context) then override placement to 1 - if (params.context === 'instream') { - video.startdelay = video.startdelay || 0; - video.placement = 1; - } - - // bid floor - const bidFloorRequest = { - currency: bidRequest.params.cur || 'USD', - mediaType: 'video', - size: '*' - }; - let floorData = bidRequest.params - if (isFn(bidRequest.getFloor)) { - floorData = bidRequest.getFloor(bidFloorRequest); - } else { - if (params.bidfloor) { - floorData = {floor: params.bidfloor, currency: params.currency || 'USD'}; - } - } - - const openrtbRequest = { - id: bidRequest.bidId, - imp: [ - { - id: '1', - video: video, - secure: isSecure() ? 1 : 0, - bidfloor: floorData.floor, - bidfloorcur: floorData.currency - } - ], - site: { - domain: bidderRequest.refererInfo.domain, - page: bidderRequest.refererInfo.page, - ref: bidderRequest.refererInfo.ref, - }, - ext: { - hb: 1, - prebidver: '$prebid.version$', - adapterver: spec.VERSION, - }, - }; - - // content - if (videoParams.content && isPlainObject(videoParams.content)) { - openrtbRequest.site.content = {}; - const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url']; - const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; - const contentArrayKeys = ['cat']; - const contentObjectKeys = ['ext']; - for (const contentKey in videoBidderParams.content) { - if ( - (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) || - (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) || - (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) || - (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) && - videoParams.content[contentKey].every(catStr => isStr(catStr)))) { - openrtbRequest.site.content[contentKey] = videoParams.content[contentKey]; - } else { - logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); - } - } - } - - return openrtbRequest; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildBannerRequestData(bidRequests, bidderRequest) { - const impr = bidRequests.map(bidRequest => ({ - id: bidRequest.bidId, - banner: { - format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ - w: sizeArr[0], - h: sizeArr[1] - })) - }, - ext: { - exchange: { - placementId: bidRequest.params.placementId - } - } - })); - - const openrtbRequest = { - id: bidderRequest.bidderRequestId, - imp: impr, - site: { - domain: bidderRequest.refererInfo?.domain, - page: bidderRequest.refererInfo?.page, - ref: bidderRequest.refererInfo?.ref, - }, - ext: {} - }; - return openrtbRequest; -} - -function _createBidResponse(bid) { - const isADomainPresent = - bid.adomain && bid.adomain.length; - const bidResponse = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - ad: bid.adm, - ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, - creativeId: bid.crid, - netRevenue: DEFAULT_NET_REVENUE, - currency: DEFAULT_CURRENCY, - mediaType: deepAccess(bid, 'ext.prebid.type', BANNER) - } - - if (isADomainPresent) { - bidResponse.meta = { - advertiserDomains: bid.adomain - }; - } - - if (bidResponse.mediaType === VIDEO) { - bidResponse.vastXml = bid.adm; - } - - return bidResponse; -} - -function isSecure() { - return document.location.protocol === 'https:'; -} - -registerBidder(spec); diff --git a/modules/lassoBidAdapter.js b/modules/lassoBidAdapter.js index e1f9636e4f1..6215af03a97 100644 --- a/modules/lassoBidAdapter.js +++ b/modules/lassoBidAdapter.js @@ -37,7 +37,6 @@ export const spec = { url: encodeURIComponent(window.location.href), bidderRequestId: bidRequest.bidderRequestId, adUnitCode: bidRequest.adUnitCode, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 auctionId: bidRequest.auctionId, bidId: bidRequest.bidId, transactionId: bidRequest.ortb2Imp?.ext?.tid, @@ -48,11 +47,20 @@ export const spec = { params: JSON.stringify(bidRequest.params), crumbs: JSON.stringify(bidRequest.crumbs), prebidVersion: '$prebid.version$', - version: 3, + version: 4, coppa: config.getConfig('coppa') == true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined } + if ( + bidderRequest && + bidderRequest.gppConsent && + bidderRequest.gppConsent.gppString + ) { + payload.gpp = bidderRequest.gppConsent.gppString; + payload.gppSid = bidderRequest.gppConsent.applicableSections; + } + return { method: 'GET', url: getBidRequestUrl(aimXR, bidRequest.params), @@ -74,6 +82,7 @@ export const spec = { const bidResponse = { requestId: response.bidid, + bidId: response.bidid, cpm: response.bid.price, currency: response.cur, width: response.bid.w, diff --git a/modules/lemmaDigitalBidAdapter.js b/modules/lemmaDigitalBidAdapter.js index 9fa3081a47e..dde7c25d9b9 100644 --- a/modules/lemmaDigitalBidAdapter.js +++ b/modules/lemmaDigitalBidAdapter.js @@ -3,6 +3,15 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + var BIDDER_CODE = 'lemmadigital'; var LOG_WARN_PREFIX = 'LEMMADIGITAL: '; var ENDPOINT = 'https://bid.lemmadigital.com/lemma/servad'; @@ -26,7 +35,7 @@ export var spec = { * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. - **/ + */ isBidRequestValid: (bid) => { if (!bid || !bid.params) { utils.logError(LOG_WARN_PREFIX, 'nil/empty bid object'); @@ -51,11 +60,11 @@ export var spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - **/ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: (validBidRequests, bidderRequest) => { if (validBidRequests.length === 0) { return; @@ -79,11 +88,11 @@ export var spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} response A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - **/ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} response A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: (response, request) => { return spec._parseRTBResponse(request, response.body); }, @@ -93,7 +102,7 @@ export var spec = { * @param {SyncOptions} syncOptions Which user syncs are allowed? * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. - **/ + */ getUserSyncs: (syncOptions, serverResponses) => { let syncurl = USER_SYNC + 'pid=' + pubId; if (syncOptions.iframeEnabled) { @@ -115,7 +124,7 @@ export var spec = { /** * parse object - **/ + */ _parseJSON: function (rawPayload) { try { if (rawPayload) { @@ -155,7 +164,7 @@ export var spec = { /** * create IAB standard OpenRTB bid request - **/ + */ _createoRTBRequest: (bidRequests, conf) => { var oRTBObject = {}; try { @@ -202,7 +211,7 @@ export var spec = { /** * create impression array objects - **/ + */ _getImpressionArray: (request) => { var impArray = []; var map = request.map(bid => spec._getImpressionObject(bid)); @@ -218,7 +227,7 @@ export var spec = { /** * create impression (single) object - **/ + */ _getImpressionObject: (bid) => { var impression = {}; var bObj; @@ -277,8 +286,8 @@ export var spec = { }, /** - * set bid floor - **/ + * set bid floor + */ _setFloor: (impObj, bid) => { let bidFloor = -1; // get lowest floor from floorModule @@ -304,8 +313,8 @@ export var spec = { }, /** - * parse Open RTB response - **/ + * parse Open RTB response + */ _parseRTBResponse: (request, response) => { var bidResponses = []; try { @@ -358,8 +367,8 @@ export var spec = { }, /** - * get bid request api end point url - **/ + * get bid request api end point url + */ _endPointURL: (request) => { var params = request && request[0].params ? request[0].params : null; if (params) { @@ -371,8 +380,8 @@ export var spec = { }, /** - * get domain name from url - **/ + * get domain name from url + */ _getDomain: (url) => { var a = document.createElement('a'); a.setAttribute('href', url); @@ -380,8 +389,8 @@ export var spec = { }, /** - * create the site object - **/ + * create the site object + */ _getSiteObject: (request, conf) => { var params = request && request.params ? request.params : null; if (params) { @@ -406,8 +415,8 @@ export var spec = { }, /** - * create the app object - **/ + * create the app object + */ _getAppObject: (request) => { var params = request && request.params ? request.params : null; if (params) { @@ -432,8 +441,8 @@ export var spec = { }, /** - * create the device object - **/ + * create the device object + */ _getDeviceObject: (request) => { var params = request && request.params ? request.params : null; if (params) { @@ -481,8 +490,8 @@ export var spec = { }, /** - * get request ad sizes - **/ + * get request ad sizes + */ _getSizes: (request) => { if (request && request.sizes && utils.isArray(request.sizes[0]) && request.sizes[0].length > 0) { return request.sizes[0]; @@ -491,8 +500,8 @@ export var spec = { }, /** - * create the banner object - **/ + * create the banner object + */ _getBannerRequest: (bid) => { var bObj; var adFormat = []; @@ -531,8 +540,8 @@ export var spec = { }, /** - * create the video object - **/ + * create the video object + */ _getVideoRequest: (bid) => { var vObj; if (utils.deepAccess(bid, 'mediaTypes.video')) { @@ -554,8 +563,8 @@ export var spec = { }, /** - * check media type - **/ + * check media type + */ _checkMediaType: (adm, newBid) => { // Create a regex here to check the strings var videoRegex = new RegExp(/VAST.*version/); diff --git a/modules/lifestreetBidAdapter.js b/modules/lifestreetBidAdapter.js index 6a8b783ce21..5b5eb639fcf 100644 --- a/modules/lifestreetBidAdapter.js +++ b/modules/lifestreetBidAdapter.js @@ -2,6 +2,10 @@ import { isInteger } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'lifestreet'; const ADAPTER_VERSION = '$prebid.version$'; diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index 0eb9e900160..5cccf5300b3 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -3,6 +3,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { ajax } from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'limelightDigital'; /** @@ -163,7 +169,8 @@ function buildPlacement(bidRequest) { custom2: bidRequest.params.custom2, custom3: bidRequest.params.custom3, custom4: bidRequest.params.custom4, - custom5: bidRequest.params.custom5 + custom5: bidRequest.params.custom5, + page: bidRequest.refererInfo.page } } } diff --git a/modules/liveIntentAnalyticsAdapter.js b/modules/liveIntentAnalyticsAdapter.js index ffe4f8f58b0..54402bcafc6 100644 --- a/modules/liveIntentAnalyticsAdapter.js +++ b/modules/liveIntentAnalyticsAdapter.js @@ -10,11 +10,8 @@ const ANALYTICS_TYPE = 'endpoint'; const URL = 'https://wba.liadm.com/analytic-events'; const GVL_ID = 148; const ADAPTER_CODE = 'liveintent'; -const DEFAULT_SAMPLING = 0.1; const DEFAULT_BID_WON_TIMEOUT = 2000; const { EVENTS: { AUCTION_END } } = CONSTANTS; -let initOptions = {}; -let isSampled; let bidWonTimeout; function handleAuctionEnd(args) { @@ -123,19 +120,15 @@ function ignoreUndefined(data) { let liAnalytics = Object.assign(adapter({URL, ANALYTICS_TYPE}), { track({ eventType, args }) { - if (eventType == AUCTION_END && args && isSampled) { handleAuctionEnd(args); } + if (eventType == AUCTION_END && args) { handleAuctionEnd(args); } } }); // save the base class function liAnalytics.originEnableAnalytics = liAnalytics.enableAnalytics; - // override enableAnalytics so we can get access to the config passed in from the page liAnalytics.enableAnalytics = function (config) { - initOptions = config.options; - const sampling = (initOptions && initOptions.sampling) ?? DEFAULT_SAMPLING; - isSampled = Math.random() < parseFloat(sampling); - bidWonTimeout = (initOptions && initOptions.bidWonTimeout) ?? DEFAULT_BID_WON_TIMEOUT; + bidWonTimeout = config?.options?.bidWonTimeout ?? DEFAULT_BID_WON_TIMEOUT; liAnalytics.originEnableAnalytics(config); // call the base class function }; diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 33a702aa81f..786feeb8052 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -8,10 +8,20 @@ import { triggerPixel, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {UID1_EIDS} from '../libraries/uid1Eids/uid1Eids.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const DEFAULT_AJAX_TIMEOUT = 5000 const EVENTS_TOPIC = 'pre_lips' const MODULE_NAME = 'liveIntentId'; const LI_PROVIDER_DOMAIN = 'liveintent.com'; @@ -65,6 +75,7 @@ function parseLiveIntentCollectorConfig(collectConfig) { collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); + config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; return config; } @@ -99,9 +110,8 @@ function initializeLiveConnect(configParams) { if (configParams.url) { identityResolutionConfig.url = configParams.url } - if (configParams.ajaxTimeout) { - identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout; - } + + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); @@ -113,6 +123,7 @@ function initializeLiveConnect(configParams) { } liveConnectConfig.wrapperName = 'prebid'; + liveConnectConfig.trackerVersion = '$prebid.version$'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; liveConnectConfig.fireEventDelay = configParams.fireEventDelay; @@ -125,7 +136,11 @@ function initializeLiveConnect(configParams) { liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; liveConnectConfig.gdprConsent = gdprConsent.consentString; } - + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + liveConnectConfig.gppString = gppConsent.gppString; + liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; + } // The second param is the storage object, LS & Cookie manipulation uses PBJS // The third param is the ajax and pixel object, the ajax and pixel use PBJS liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); @@ -150,7 +165,7 @@ function tryFireEvent() { /** @type {Submodule} */ export const liveIntentIdSubmodule = { - moduleMode: process.env.LiveConnectMode, + moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', /** * used to link submodule with config * @type {string} @@ -205,6 +220,28 @@ export const liveIntentIdSubmodule = { result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } } + if (value.index) { + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.openx) { + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.pubmatic) { + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.sovrn) { + result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.thetradedesk) { + result.lipb = {...result.lipb, tdid: value.thetradedesk} + result.tdid = { 'id': value.thetradedesk, ext: { rtiPartner: 'TDID', provider: getRefererInfo().domain || LI_PROVIDER_DOMAIN } } + delete result.lipb.thetradedesk + } + return result } @@ -244,6 +281,8 @@ export const liveIntentIdSubmodule = { return { callback: result }; }, eids: { + ...UID1_EIDS, + ...UID2_EIDS, 'lipb': { getValue: function(data) { return data.lipbid; @@ -294,7 +333,54 @@ export const liveIntentIdSubmodule = { } } }, - + 'index': { + source: 'liveintent.indexexchange.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'openx': { + source: 'openx.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'pubmatic': { + source: 'pubmatic.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'sovrn': { + source: 'liveintent.sovrn.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } } }; diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 82affe40e03..cfbd2b5b3b5 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -4,7 +4,11 @@ import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'livewrapped'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -47,9 +51,6 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function(bidRequests, bidderRequest) { - // convert Native ORTB definition to old-style prebid native definition - bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); - const userId = find(bidRequests, hasUserId); const pubcid = find(bidRequests, hasPubcid); const publisherId = find(bidRequests, hasPublisherId); @@ -231,9 +232,9 @@ function bidToAdRequest(bid, currency) { adUnitId: bid.params.adUnitId, callerAdUnitId: bid.params.adUnitName || bid.adUnitCode || bid.placementCode, bidId: bid.bidId, - transactionId: bid.ortb2Imp?.ext?.tid, formats: getSizes(bid).map(sizeToFormat), flr: getBidFloor(bid, currency), + rtbData: bid.ortb2Imp, options: bid.params.options }; diff --git a/modules/lm_kiviadsBidAdapter.js b/modules/lm_kiviadsBidAdapter.js new file mode 100644 index 00000000000..7c3085047c4 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.js @@ -0,0 +1,215 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const CUR = 'USD'; +const BIDDER_CODE = 'lm_kiviads'; +const ENDPOINT = 'https://pbjs.kiviads.live'; + +/** + * 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. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['kivi'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/lm_kiviadsBidAdapter.md b/modules/lm_kiviadsBidAdapter.md new file mode 100644 index 00000000000..fc1b05d1ef7 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: lm_kiviads Bidder Adapter +Module Type: lm_kiviads Bidder Adapter +Maintainer: pavlo@xe.works +``` + +# Description + +Module that connects to kiviads.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/lockerdomeBidAdapter.js b/modules/lockerdomeBidAdapter.js index 5c38753c1e2..5038eadce30 100644 --- a/modules/lockerdomeBidAdapter.js +++ b/modules/lockerdomeBidAdapter.js @@ -1,6 +1,6 @@ -import { getBidIdParameter } from '../src/utils.js'; import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; export const spec = { code: 'lockerdome', diff --git a/modules/logicadBidAdapter.js b/modules/logicadBidAdapter.js index 07f9b893887..fe4dd83c9e2 100644 --- a/modules/logicadBidAdapter.js +++ b/modules/logicadBidAdapter.js @@ -31,13 +31,25 @@ export const spec = { }, interpretResponse: function (serverResponse, bidderRequest) { serverResponse = serverResponse.body; + const bids = []; + if (!serverResponse || serverResponse.error) { return bids; } + serverResponse.seatbid.forEach(function (seatbid) { bids.push(seatbid.bid); }) + + const fledgeAuctionConfigs = deepAccess(serverResponse, 'ext.fledgeAuctionConfigs') || []; + if (fledgeAuctionConfigs.length) { + return { + bids, + fledgeAuctionConfigs, + }; + } + return bids; }, getUserSyncs: function (syncOptions, serverResponses) { @@ -52,32 +64,42 @@ export const spec = { }, }; -function newBidRequest(bid, bidderRequest) { +function newBidRequest(bidRequest, bidderRequest) { + const bid = { + adUnitCode: bidRequest.adUnitCode, + bidId: bidRequest.bidId, + transactionId: bidRequest.ortb2Imp?.ext?.tid, + sizes: bidRequest.sizes, + params: bidRequest.params, + mediaTypes: bidRequest.mediaTypes, + } + + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + if (fledgeEnabled) { + const ae = deepAccess(bidRequest, 'ortb2Imp.ext.ae'); + if (ae) { + bid.ae = ae; + } + } + const data = { // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, - bidderRequestId: bid.bidderRequestId, - bids: [{ - adUnitCode: bid.adUnitCode, - bidId: bid.bidId, - transactionId: bid.ortb2Imp?.ext?.tid, - sizes: bid.sizes, - params: bid.params, - mediaTypes: bid.mediaTypes - }], + auctionId: bidRequest.auctionId, + bidderRequestId: bidRequest.bidderRequestId, + bids: [bid], prebidJsVersion: '$prebid.version$', // TODO: is 'page' the right value here? referrer: bidderRequest.refererInfo.page, auctionStartTime: bidderRequest.auctionStart, - eids: bid.userIdAsEids, + eids: bidRequest.userIdAsEids, }; - const sua = deepAccess(bid, 'ortb2.device.sua'); + const sua = deepAccess(bidRequest, 'ortb2.device.sua'); if (sua) { data.sua = sua; } - const userData = deepAccess(bid, 'ortb2.user.data'); + const userData = deepAccess(bidRequest, 'ortb2.user.data'); if (userData) { data.userData = userData; } diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 808a67492b0..64d631c2469 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -20,6 +20,13 @@ import {getStorageManager} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const KEY_ID = 'panoramaId'; const KEY_EXPIRY = `${KEY_ID}_expiry`; const KEY_PROFILE = '_cc_id'; diff --git a/modules/loyalBidAdapter.js b/modules/loyalBidAdapter.js new file mode 100644 index 00000000000..30fdeb44233 --- /dev/null +++ b/modules/loyalBidAdapter.js @@ -0,0 +1,190 @@ +import { logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'loyal'; +const AD_URL = 'https://us-east-1.loyal.app/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 || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor, + eids: [] + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + tmax: bidderRequest.timeout + }; + + if (bidderRequest.gdprConsent?.consentString) { + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + 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)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, +}; + +registerBidder(spec); diff --git a/modules/loyalBidAdapter.md b/modules/loyalBidAdapter.md new file mode 100644 index 00000000000..db77c04c34f --- /dev/null +++ b/modules/loyalBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Loyal Bidder Adapter +Module Type: Loyal Bidder Adapter +Maintainer: hello@loyal.app +``` + +# Description + +Connects to Loyal exchange for bids. +Loyal bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'loyal', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'loyal', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'loyal', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/luceadBidAdapter.js b/modules/luceadBidAdapter.js new file mode 100644 index 00000000000..ab7f96c4e60 --- /dev/null +++ b/modules/luceadBidAdapter.js @@ -0,0 +1,164 @@ +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getUniqueIdentifierStr, logInfo, deepSetValue} from '../src/utils.js'; +import {fetch} from '../src/ajax.js'; + +const bidderCode = 'lucead'; +const bidderName = 'Lucead'; +let baseUrl = 'https://lucead.com'; +let staticUrl = 'https://s.lucead.com'; +let companionUrl = 'https://cdn.jsdelivr.net/gh/lucead/prebid-js-external-js-lucead@master/dist/prod.min.js'; +let endpointUrl = 'https://prebid.lucead.com/go'; +const defaultCurrency = 'EUR'; +const defaultTtl = 500; +const aliases = ['adliveplus']; + +function isDevEnv() { + return location.hash.includes('prebid-dev') || location.href.startsWith('https://ayads.io/test'); +} + +function isBidRequestValid(bidRequest) { + return !!bidRequest?.params?.placementId; +} + +export function log(msg, obj) { + logInfo(`${bidderName} - ${msg}`, obj); +} + +function buildRequests(bidRequests, bidderRequest) { + if (isDevEnv()) { + baseUrl = location.origin; + staticUrl = baseUrl; + companionUrl = `${staticUrl}/dist/prebid-companion.js`; + endpointUrl = `${baseUrl}/go`; + } + + log('buildRequests', { + bidRequests, + bidderRequest, + }); + + const companionData = { + base_url: baseUrl, + static_url: staticUrl, + endpoint_url: endpointUrl, + request_id: bidderRequest.bidderRequestId, + prebid_version: '$prebid.version$', + bidRequests, + bidderRequest, + getUniqueIdentifierStr, + ortbConverter, + deepSetValue, + }; + + loadExternalScript(companionUrl, bidderCode, () => window.ayads_prebid && window.ayads_prebid(companionData)); + + return bidRequests.map(bidRequest => ({ + method: 'POST', + url: `${endpointUrl}/prebid/sub`, + data: JSON.stringify({ + request_id: bidderRequest.bidderRequestId, + domain: location.hostname, + bid_id: bidRequest.bidId, + sizes: bidRequest.sizes, + media_types: bidRequest.mediaTypes, + fledge_enabled: bidderRequest.fledgeEnabled, + enable_contextual: bidRequest?.params?.enableContextual !== false, + enable_pa: bidRequest?.params?.enablePA !== false, + params: bidRequest.params, + }), + options: { + contentType: 'text/plain', + withCredentials: false + }, + })); +} + +function interpretResponse(serverResponse, bidRequest) { + // @see required fields https://docs.prebid.org/dev-docs/bidder-adaptor.html + const response = serverResponse.body; + const bidRequestData = JSON.parse(bidRequest.data); + + const bids = response.enable_contextual !== false ? [{ + requestId: response?.bid_id || '1', // bid request id, the bid id + cpm: response?.cpm || 0, + width: (response?.size && response?.size?.width) || 300, + height: (response?.size && response?.size?.height) || 250, + currency: response?.currency || defaultCurrency, + ttl: response?.ttl || defaultTtl, + creativeId: response.ssp ? `ssp:${response.ssp}` : (response?.ad_id || '0'), + netRevenue: response?.netRevenue || true, + ad: response?.ad || '', + meta: { + advertiserDomains: response?.advertiserDomains || [], + }, + }] : null; + + log('interpretResponse', {serverResponse, bidRequest, bidRequestData, bids}); + + if (response.enable_pa === false) { return bids; } + + const fledgeAuctionConfig = { + seller: baseUrl, + decisionLogicUrl: `${baseUrl}/js/ssp.js`, + interestGroupBuyers: [baseUrl], + perBuyerSignals: {}, + auctionSignals: { + size: bidRequestData.sizes ? {width: bidRequestData?.sizes[0][0] || 300, height: bidRequestData?.sizes[0][1] || 250} : null, + }, + }; + + const fledgeAuctionConfigs = [{bidId: response.bid_id, config: fledgeAuctionConfig}]; + + return {bids, fledgeAuctionConfigs}; +} + +function report(type = 'impression', data = {}) { + // noinspection JSCheckFunctionSignatures + return fetch(`${endpointUrl}/report/${type}`, { + body: JSON.stringify(data), + method: 'POST', + contentType: 'text/plain' + }); +} + +function onBidWon(bid) { + log('Bid won', bid); + + let data = { + bid_id: bid?.bidId, + placement_id: bid?.params ? bid?.params[0]?.placementId : 0, + spent: bid?.cpm, + currency: bid?.currency, + }; + + if (bid.creativeId) { + if (bid.creativeId.toString().startsWith('ssp:')) { + data.ssp = bid.creativeId.split(':')[1]; + } else { + data.ad_id = bid.creativeId; + } + } + + return report(`impression`, data); +} + +function onTimeout(timeoutData) { + log('Timeout from adapter', timeoutData); +} + +export const spec = { + code: bidderCode, + // gvlid: BIDDER_GVLID, + aliases, + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, + onTimeout, + isDevEnv, +}; + +// noinspection JSCheckFunctionSignatures +registerBidder(spec); diff --git a/modules/luceadBidAdapter.md b/modules/luceadBidAdapter.md new file mode 100644 index 00000000000..953c911cd2b --- /dev/null +++ b/modules/luceadBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +Module Name: Lucead Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid@lucead.com + +# Description + +Module that connects to Lucead demand source to fetch bids. + +# Test Parameters +``` +const adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: 'lucead', + params: { + placementId: '2', + } + } + ] + } + ]; +``` diff --git a/modules/madvertiseBidAdapter.js b/modules/madvertiseBidAdapter.js index 457ff2409b8..3b031623aef 100644 --- a/modules/madvertiseBidAdapter.js +++ b/modules/madvertiseBidAdapter.js @@ -2,6 +2,11 @@ import { parseSizesInput, _each } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + // use protocol relative urls for http or https const MADVERTISE_ENDPOINT = 'https://mobile.mng-ads.com/'; diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index b501e3bef5a..5cc45e3adbf 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -25,6 +25,7 @@ import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +import { getHook } from '../src/hook.js'; const RUBICON_GVL_ID = 52; export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'magnite' }); @@ -45,6 +46,7 @@ const pbsErrorMap = { 4: 'request-error', 999: 'generic-error' } +let cookieless; let prebidGlobal = getGlobal(); const { @@ -75,7 +77,8 @@ const resetConfs = () => { pendingEvents: {}, eventPending: false, elementIdMap: {}, - sessionData: {} + sessionData: {}, + bidsCachedClientSide: new WeakSet() } rubiConf = { pvid: generateUUID().slice(0, 8), @@ -142,6 +145,10 @@ const sendEvent = payload => { ...getTopLevelDetails(), ...payload } + if (window.pbjs?.rp?.eventDispatcher) { + const analyticsEvent = new CustomEvent('beforeSendingMagniteAnalytics', { detail: event }); + window.pbjs.rp.eventDispatcher.dispatchEvent(analyticsEvent); + } ajax( endpoint, null, @@ -326,10 +333,14 @@ const getTopLevelDetails = () => { // Add DM wrapper details if (rubiConf.wrapperName) { + let rule; + if (cookieless) { + rule = rubiConf.rule_name ? rubiConf.rule_name.concat('_cookieless') : 'cookieless'; + } payload.wrapper = { name: rubiConf.wrapperName, family: rubiConf.wrapperFamily, - rule: rubiConf.rule_name + rule } } @@ -667,8 +678,20 @@ function enableMgniAnalytics(config = {}) { window.googletag.cmd = window.googletag.cmd || []; window.googletag.cmd.push(() => subscribeToGamSlots()); } + + // Edge case handler for client side video caching + getHook('callPrebidCache').before(callPrebidCacheHook); }; +/* + We want to know if a bid was cached client side + And if it was we will use the actual bidId instead of the pbsBidId override in our BID_RESPONSE handler +*/ +export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, videoMediaType) { + cache.bidsCachedClientSide.add(bidResponse); + fn.call(this, auctionInstance, bidResponse, afterBidAdded, videoMediaType); +} + const handleBidWon = args => { const bidWon = formatBidWon(args); addEventToQueue({ bidsWon: [bidWon] }, bidWon.renderAuctionId, 'bidWon'); @@ -683,6 +706,7 @@ magniteAdapter.disableAnalytics = function () { endpoint = undefined; accountId = undefined; resetConfs(); + getHook('callPrebidCache').getHooks({ hook: callPrebidCacheHook }).remove(); magniteAdapter.originDisableAnalytics(); }; @@ -745,7 +769,7 @@ const handleBidResponse = (args, bidStatus) => { // if pbs gave us back a bidId, we need to use it and update our bidId to PBA const pbsBidId = (args.pbsBidId == 0 ? generateUUID() : args.pbsBidId) || (args.seatBidId == 0 ? generateUUID() : args.seatBidId); - if (pbsBidId) { + if (pbsBidId && !cache.bidsCachedClientSide.has(args)) { bid.pbsBidId = pbsBidId; } } @@ -804,6 +828,15 @@ magniteAdapter.track = ({ eventType, args }) => { auctionData.floors = addFloorData(floorData); } + // Identify chrome cookieless trafic + if (!cookieless) { + const cdep = deepAccess(args, 'bidderRequests.0.ortb2.device.ext.cdep'); + if (cdep && (cdep.indexOf('treatment') !== -1 || cdep.indexOf('control_2') !== -1)) { + cookieless = 1; + auctionData.cdep = 1; + } + } + // GDPR info const gdprData = deepAccess(args, 'bidderRequests.0.gdprConsent'); if (gdprData) { diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 5ac50936ed6..67c8a4aec07 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -2,6 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'malltv'; const ENDPOINT_URL = 'https://central.mall.tv/bid'; const DIMENSION_SEPARATOR = 'x'; @@ -124,11 +131,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/mediabramaBidAdapter.js b/modules/mediabramaBidAdapter.js new file mode 100644 index 00000000000..caf6854fe03 --- /dev/null +++ b/modules/mediabramaBidAdapter.js @@ -0,0 +1,155 @@ +import { + isFn, + isStr, + deepAccess, + getWindowTop, + triggerPixel +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'mediabrama'; +const AD_URL = 'https://prebid.mediabrama.com/pbjs'; +const SYNC_URL = 'https://prebid.mediabrama.com/sync'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency || !bid.meta) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + default: + return false; + } +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placementId); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = getWindowTop(); + const location = winTop.location; + const placements = []; + + const request = { + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + host: location.host, + page: location.pathname, + placements: placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent; + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + bidfloor: getBidFloor(bid) + }; + + if (typeof bid.userId !== 'undefined') { + placement.userId = bid.userId; + } + + const mediaType = bid.mediaTypes; + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.adFormat = BANNER; + } + + 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)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidWon: (bid) => { + const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); + triggerPixel(bid.nurl); + } + } +}; + +registerBidder(spec); diff --git a/modules/mediabramaBidAdapter.md b/modules/mediabramaBidAdapter.md new file mode 100644 index 00000000000..fde0a399852 --- /dev/null +++ b/modules/mediabramaBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaBrama Bidder Adapter +Module Type: MediaBrama Bidder Adapter +Maintainer: support@mediabrama.com +``` + +# Description + +Module that connects to mediabrama demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'div-prebid', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mediabrama', + params: { + placementId: '24428' //test, please replace after test + } + } + ] + }, + ]; +``` diff --git a/modules/mediafilterRtdProvider.js b/modules/mediafilterRtdProvider.js new file mode 100644 index 00000000000..8a082ad4d59 --- /dev/null +++ b/modules/mediafilterRtdProvider.js @@ -0,0 +1,94 @@ +/** + * This module adds the Media Filter real-time ad monitoring and protection module. + * + * The {@link module:modules/realTimeData} module is required + * + * For more information, visit {@link https://www.themediatrust.com The Media Trust}. + * + * @author Mirnes Cajlakovic + * @module modules/mediafilterRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { logError, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +/** The event type for Media Filter. */ +export const MEDIAFILTER_EVENT_TYPE = 'com.mediatrust.pbjs.'; +/** The base URL for Media Filter scripts. */ +export const MEDIAFILTER_BASE_URL = 'https://scripts.webcontentassessor.com/scripts/'; + +export const MediaFilter = { + /** + * Registers the Media Filter as a submodule of real-time data. + */ + register: function() { + submodule('realTimeData', { + 'name': 'mediafilter', + 'init': this.generateInitHandler() + }); + }, + + /** + * Sets up the Media Filter by initializing event listeners and loading the external script. + * @param {object} configuration - The configuration object. + */ + setup: function(configuration) { + this.setupEventListener(configuration.configurationHash); + this.setupScript(configuration.configurationHash); + }, + + /** + * Sets up an event listener for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + */ + setupEventListener: function(configurationHash) { + window.addEventListener('message', this.generateEventHandler(configurationHash)); + }, + + /** + * Loads the Media Filter script based on the provided configuration hash. + * @param {string} configurationHash - The configuration hash. + */ + setupScript: function(configurationHash) { + loadExternalScript(MEDIAFILTER_BASE_URL.concat(configurationHash), 'mediafilter', () => {}); + }, + + /** + * Generates an event handler for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + * @returns {function} The generated event handler. + */ + generateEventHandler: function(configurationHash) { + return (windowEvent) => { + if (windowEvent.data.type === MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash)) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': generateUUID(), + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + }); + } + }; + }, + + /** + * Generates an initialization handler for Media Filter. + * @returns {function} The generated init handler. + */ + generateInitHandler: function() { + return (configuration) => { + try { + this.setup(configuration); + } catch (error) { + logError(`Error in initialization: ${error.message}`); + } + }; + } +}; + +// Register the module +MediaFilter.register(); diff --git a/modules/mediafilterRtdProvider.md b/modules/mediafilterRtdProvider.md new file mode 100644 index 00000000000..469479f8d0b --- /dev/null +++ b/modules/mediafilterRtdProvider.md @@ -0,0 +1,37 @@ +## Overview + +**Module:** The Media Filter +**Type: **Real Time Data Module + +As malvertising, scams, and controversial and offensive ad content proliferate across the digital media ecosystem, publishers need advanced controls to both shield audiences from malware attacks and ensure quality site experience. With the market’s fastest and most comprehensive real-time ad quality tool, The Media Trust empowers publisher Ad/Revenue Operations teams to block a wide range of malware, high-risk ad platforms, heavy ads, ads with sensitive or objectionable content, and custom lists (e.g., competitors). Customizable replacement code calls for a new ad to ensure impressions are still monetized. + +[![IMAGE ALT TEXT](http://img.youtube.com/vi/VBHRiirge7s/0.jpg)](http://www.youtube.com/watch?v=VBHRiirge7s "Publishers' Ultimate Avenger: Media Filter") + +To start using this module, please contact [The Media Trust](https://mediatrust.com/how-we-help/media-filter/ "The Media Trust") to get a script and configuration hash for module configuration. + +## Integration + +1. Build Prebid bundle with The Media Filter module included. + +``` +gulp build --modules=mediafilterRtdProvider +``` + +2. Inlcude the bundled script in your application. + +## Configuration + +Add configuration entry to `realTimeData.dataProviders` for The Media Filter module. + +``` +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'mediafilter', + params: { + configurationHash: '', + } + }] + } +}); +``` diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 3d33bbf8c12..9f899974721 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -3,6 +3,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'mediaforce'; const ENDPOINT_URL = 'https://rtb.mfadsrvr.com/header_bid'; const TEST_ENDPOINT_URL = 'https://rtb.mfadsrvr.com/header_bid?debug_key=abcdefghijklmnop'; diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index 98179c49e0d..5e31f60d3b5 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -1,14 +1,8 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -26,7 +20,6 @@ import {Renderer} from '../src/Renderer.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -38,7 +31,15 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'mediafuse'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -868,9 +869,7 @@ function bidToTag(bid) { tag['banner_frameworks'] = bid.params.frameworks; } - // TODO: why does this need to iterate through every ad unit? - let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); - if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { + if (bid.mediaTypes?.banner) { tag.ad_types.push(BANNER); } @@ -959,7 +958,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -985,7 +984,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 756e636572d..8f687d30ff3 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -8,39 +8,115 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; // import { config } from '../src/config.js'; // import { isPubcidEnabled } from './pubCommonId.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').mediaType} mediaType + */ + const BIDDER_CODE = 'mediago'; // const PROTOCOL = window.document.location.protocol; -const ENDPOINT_URL = - // ((PROTOCOL === 'https:') ? 'https' : 'http') + - 'https://rtb-us.mediago.io/api/bid?tn='; +const ENDPOINT_URL = 'https://gbid.mediago.io/api/bid?tn='; +// const COOKY_SYNC_URL = 'https://gtrace.mediago.io/ju/cs/eplist'; +const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; +export const THIRD_PARTY_COOKIE_ORIGIN = 'https://cdn.mediago.io'; + const TIME_TO_LIVE = 500; +const GVLID = 1020; // const ENDPOINT_URL = '/api/bid?tn='; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; /* ----- mguid:start ------ */ -const COOKIE_KEY_MGUID = '__mguid_'; +export const COOKIE_KEY_MGUID = '__mguid_'; +const COOKIE_KEY_PMGUID = '__pmguid_'; +const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year +let reqTimes = 0; +/** + * get page title + * @returns {string} + */ + +export function getPageTitle(win = window) { + try { + const ogTitle = win.top.document.querySelector('meta[property="og:title"]') + return win.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + return document.title || (ogTitle && ogTitle.content) || ''; + } +} /** - * čŽˇå–į”¨æˆˇid - * @return {string} + * get page description + * + * @returns {string} + */ +export function getPageDescription(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="description"]') || + win.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +/** + * get page keywords + * @returns {string} */ -const getUserID = () => { - const i = storage.getCookie(COOKIE_KEY_MGUID); +export function getPageKeywords(win = window) { + let element; - if (i === null) { - const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid); - return uuid; + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); } - return i; + + return (element && element.content) || ''; +} + +/** + * get connection downlink + * @returns {number} + */ +export function getConnectionDownLink(win = window) { + const nav = win.navigator || {}; + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; +} + +/** + * get pmg uid + * čŽˇå–åšļį”Ÿæˆį”¨æˆˇįš„id + * + * @return {string} + */ +export const getPmgUID = () => { + if (!storage.cookiesAreEnabled()) return; + + let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID); + if (!pmgUid) { + pmgUid = utils.generateUUID(); + try { + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString()); + } catch (e) {} + } + return pmgUid; }; -/* ----- mguid:end ------ */ +/* ----- pmguid:end ------ */ /** * čŽˇå–ä¸€ä¸Ēå¯ščąĄįš„某ä¸Ēå€ŧīŧŒåĻ‚æžœæ˛Ąæœ‰åˆ™čŋ”回įŠē字įŦĻ串 + * * @param {Object} obj å¯ščąĄ * @param {...string} keys 锎名 * @return {any} @@ -72,7 +148,7 @@ function isMobileAndTablet() { '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', - '|windows ce|xda|xiino|android|ipad|playbook|silk', + '|windows ce|xda|xiino|android|ipad|playbook|silk' ].join(''), 'i' ); @@ -96,7 +172,7 @@ function isMobileAndTablet() { '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', - '|your|zeto|zte-', + '|your|zeto|zte-' ].join(''), 'i' ); @@ -138,7 +214,7 @@ function getBidFloor(bid) { const bidFloor = bid.getFloor({ currency: 'USD', mediaType: '*', - size: '*', + size: '*' }); return bidFloor.floor; } catch (_) { @@ -156,11 +232,7 @@ function transformSizes(requestSizes) { let sizes = []; let sizeObj = {}; - if ( - utils.isArray(requestSizes) && - requestSizes.length === 2 && - !utils.isArray(requestSizes[0]) - ) { + 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); @@ -187,7 +259,7 @@ const mediagoAdSize = [ { w: 160, h: 600 }, { w: 320, h: 180 }, { w: 320, h: 100 }, - { w: 336, h: 280 }, + { w: 336, h: 280 } ]; /** @@ -207,24 +279,32 @@ function getItems(validBidRequests, bidderRequest) { // įĄŽčŽ¤å°ē寸是åĻįŦĻ合我äģŦčĻæą‚ for (let size of sizes) { - matchSize = mediagoAdSize.find( - (item) => size.width === item.w && size.height === item.h - ); + matchSize = mediagoAdSize.find(item => size.width === item.w && size.height === item.h); if (matchSize) { break; } } if (!matchSize) { - matchSize = sizes[0] - ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } - : { h: 0, w: 0 }; + matchSize = sizes[0] ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } : { h: 0, w: 0 }; } const bidFloor = getBidFloor(req); - // const gpid = - // utils.deepAccess(req, 'ortb2Imp.ext.gpid') || - // utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || - // utils.deepAccess(req, 'params.placementId', 0); + const gpid = + utils.deepAccess(req, 'ortb2Imp.ext.gpid') || + utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || + utils.deepAccess(req, 'params.placementId', 0); + + const gdprConsent = {}; + if (bidderRequest && bidderRequest.gdprConsent) { + gdprConsent.consent = bidderRequest.gdprConsent.consentString; + gdprConsent.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + // if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + // let ac = bidderRequest.gdprConsent.addtlConsent; + // // pull only the ids from the string (after the ~) and convert them to an array of ints + // let acStr = ac.substring(ac.indexOf('~') + 1); + // gdpr_consent.addtl_consent = acStr.split('.').map(id => parseInt(id, 10)); + // } + } // if (mediaTypes.native) {} // banneråšŋ告įąģ型 @@ -237,16 +317,21 @@ function getItems(validBidRequests, bidderRequest) { h: matchSize.h, w: matchSize.w, pos: 1, - format: sizes, + format: sizes }, ext: { - // gpid: gpid, // 加å…Ĩ后无æŗ•čŋ”回åšŋ告 + adUnitCode: req.adUnitCode, + referrer: getReferrer(req, bidderRequest), + ortb2Imp: utils.deepAccess(req, 'ortb2Imp'), // äŧ å…ĨåŽŒæ•´å¯ščąĄīŧŒåˆ†æžæ—Ĩåŋ—数捎 + gpid: gpid, // 加å…Ĩ后无æŗ•čŋ”回åšŋ告 + adslot: utils.deepAccess(req, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + ...gdprConsent // gdpr }, - tagid: req.params && req.params.tagid, + tagid: req.params && req.params.tagid }; itemMaps[id] = { req, - ret, + ret }; } @@ -255,6 +340,31 @@ function getItems(validBidRequests, bidderRequest) { return items; } +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + +/** + * get current time to UTC string + * @returns utc string + */ +export function getCurrentTimeToUTCString() { + const date = new Date(); + date.setTime(date.getTime() + COOKIE_RETENTION_TIME); + return date.toUTCString(); +} + /** * čŽˇå–rtbč¯ˇæą‚å‚æ•° * @@ -267,7 +377,14 @@ function getParam(validBidRequests, bidderRequest) { const sharedid = utils.deepAccess(validBidRequests[0], 'userId.sharedid.id') || utils.deepAccess(validBidRequests[0], 'userId.pubcid'); - const eids = validBidRequests[0].userIdAsEids || validBidRequests[0].userId; + + const bidsUserIdAsEids = validBidRequests[0].userIdAsEids; + const bidsUserid = validBidRequests[0].userId; + const eids = bidsUserIdAsEids || bidsUserid; + const ppuid = bidsUserid && bidsUserid.pubProvidedId; + const content = utils.deepAccess(bidderRequest, 'ortb2.site.content'); + const cat = utils.deepAccess(bidderRequest, 'ortb2.site.cat'); + reqTimes += 1; let isMobile = isMobileAndTablet() ? 1 : 0; // input test status by Publisher. more frequently for test true req @@ -275,14 +392,17 @@ function getParam(validBidRequests, bidderRequest) { let auctionId = getProperty(bidderRequest, 'auctionId'); let items = getItems(validBidRequests, bidderRequest); - const domain = - utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; + const domain = utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; const location = utils.deepAccess(bidderRequest, 'refererInfo.location'); const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const timeout = bidderRequest.timeout || 2000; const firstPartyData = bidderRequest.ortb2; + const topWindow = window.top; + const title = getPageTitle(); + const desc = getPageDescription(); + const keywords = getPageKeywords(); if (items && items.length) { let c = { @@ -300,14 +420,32 @@ function getParam(validBidRequests, bidderRequest) { // ua: 'Mozilla/5.0 (Linux; Android 12; SM-G970U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36', os: navigator.platform || '', ua: navigator.userAgent, - language: /en/.test(navigator.language) ? 'en' : navigator.language, + language: /en/.test(navigator.language) ? 'en' : navigator.language }, ext: { eids, + bidsUserIdAsEids, + bidsUserid, + ppuid, firstPartyData, + content, + cat, + reqTimes, + pmguid: getPmgUID(), + page: { + title: title ? title.slice(0, 100) : undefined, + desc: desc ? desc.slice(0, 300) : undefined, + keywords: keywords ? keywords.slice(0, 100) : undefined, + hLen: topWindow.history?.length || undefined, + }, + device: { + nbw: getConnectionDownLink(), + hc: topWindow.navigator?.hardwareConcurrency || undefined, + dm: topWindow.navigator?.deviceMemory || undefined, + } }, user: { - buyeruid: getUserID(), + buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, id: sharedid || pubcid, }, eids, @@ -321,11 +459,11 @@ function getParam(validBidRequests, bidderRequest) { publisher: { // todo id: domain, - name: domain, - }, + name: domain + } }, imp: items, - tmax: timeout, + tmax: timeout }; return c; } else { @@ -335,6 +473,7 @@ function getParam(validBidRequests, bidderRequest) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, // aliases: ['ex'], // short code /** * Determines whether or not the given bid request is valid. @@ -372,7 +511,6 @@ export const spec = { /** * 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. */ @@ -397,7 +535,7 @@ export const spec = { ttl: TIME_TO_LIVE, // referrer: REFERER, ad: getProperty(bid, 'adm'), - nurl: getProperty(bid, 'nurl'), + nurl: getProperty(bid, 'nurl') // adserverTargeting: { // granularityMultiplier: 0.1, // priceGranularity: 'pbHg', @@ -414,6 +552,45 @@ export const spec = { return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + window.addEventListener('message', function handler(event) { + if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + this.removeEventListener('message', handler); + + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + }, true); + return [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + } + }, + /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data @@ -433,7 +610,7 @@ export const spec = { if (bid['nurl']) { utils.triggerPixel(bid['nurl']); } - }, + } /** * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder diff --git a/modules/mediaimpactBidAdapter.js b/modules/mediaimpactBidAdapter.js new file mode 100644 index 00000000000..4ce11201507 --- /dev/null +++ b/modules/mediaimpactBidAdapter.js @@ -0,0 +1,216 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { buildUrl } from '../src/utils.js' +import {ajax} from '../src/ajax.js'; + +const BIDDER_CODE = 'mediaimpact'; +export const ENDPOINT_PROTOCOL = 'https'; +export const ENDPOINT_DOMAIN = 'bidder.smartytouch.co'; +export const ENDPOINT_PATH = '/hb/bid'; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function (bidRequest) { + return !!parseInt(bidRequest.params.unitId) || !!parseInt(bidRequest.params.partnerId); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + // TODO does it make sense to fall back to window.location.href? + const referer = bidderRequest?.refererInfo?.page || window.location.href; + + let bidRequests = []; + let beaconParams = { + tag: [], + partner: [], + sizes: [], + referer: '' + }; + + validBidRequests.forEach(function(validBidRequest) { + let sizes = validBidRequest.sizes; + if (typeof validBidRequest.params.sizes !== 'undefined') { + sizes = validBidRequest.params.sizes; + } + + let bidRequestObject = { + adUnitCode: validBidRequest.adUnitCode, + sizes: sizes, + bidId: validBidRequest.bidId, + referer: referer + }; + + if (parseInt(validBidRequest.params.unitId)) { + bidRequestObject.unitId = parseInt(validBidRequest.params.unitId); + beaconParams.tag.push(validBidRequest.params.unitId); + } + + if (parseInt(validBidRequest.params.partnerId)) { + bidRequestObject.unitId = 0; + bidRequestObject.partnerId = parseInt(validBidRequest.params.partnerId); + beaconParams.partner.push(validBidRequest.params.partnerId); + } + + bidRequests.push(bidRequestObject); + + beaconParams.sizes.push(spec.joinSizesToString(sizes)); + beaconParams.referer = encodeURIComponent(referer); + }); + + if (beaconParams.partner.length > 0) { + beaconParams.partner = beaconParams.partner.join(','); + } else { + delete beaconParams.partner; + } + + beaconParams.tag = beaconParams.tag.join(','); + beaconParams.sizes = beaconParams.sizes.join(','); + + let adRequestUrl = buildUrl({ + protocol: ENDPOINT_PROTOCOL, + hostname: ENDPOINT_DOMAIN, + pathname: ENDPOINT_PATH, + search: beaconParams + }); + + return { + method: 'POST', + url: adRequestUrl, + data: JSON.stringify(bidRequests) + }; + }, + + joinSizesToString: function(sizes) { + let res = []; + sizes.forEach(function(size) { + res.push(size.join('x')); + }); + + return res.join('|'); + }, + + interpretResponse: function (serverResponse, bidRequest) { + const validBids = JSON.parse(bidRequest.data); + + if (typeof serverResponse.body === 'undefined') { + return []; + } + + return validBids + .map(bid => ({ + bid: bid, + ad: serverResponse.body[bid.adUnitCode] + })) + .filter(item => item.ad) + .map(item => spec.adResponse(item.bid, item.ad)); + }, + + adResponse: function(bid, ad) { + const bidObject = { + requestId: bid.bidId, + ad: ad.ad, + cpm: ad.cpm, + width: ad.width, + height: ad.height, + ttl: 60, + creativeId: ad.creativeId, + netRevenue: ad.netRevenue, + currency: ad.currency, + winNotification: ad.winNotification + } + + bidObject.meta = {}; + if (ad.adomain && ad.adomain.length > 0) { + bidObject.meta.advertiserDomains = ad.adomain; + } + + return bidObject; + }, + + onBidWon: function(data) { + data.winNotification.forEach(function(unitWon) { + let adBidWonUrl = buildUrl({ + protocol: ENDPOINT_PROTOCOL, + hostname: ENDPOINT_DOMAIN, + pathname: unitWon.path + }); + + if (unitWon.method === 'POST') { + spec.postRequest(adBidWonUrl, JSON.stringify(unitWon.data)); + } + }); + + return true; + }, + + postRequest(endpoint, data) { + ajax(endpoint, null, data, {method: 'POST'}); + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncs; + } + + let appendGdprParams = function (url, gdprParams) { + if (gdprParams === null) { + return url; + } + + return url + (url.indexOf('?') >= 0 ? '&' : '?') + gdprParams; + }; + + let gdprParams = null; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; + } + } + + serverResponses.forEach(resp => { + if (resp.body) { + Object.keys(resp.body).map(function(key, index) { + let respObject = resp.body[key]; + if (respObject['syncs'] !== undefined && + Array.isArray(respObject.syncs) && + respObject.syncs.length > 0) { + if (syncOptions.iframeEnabled) { + respObject.syncs.filter(function (syncIframeObject) { + if (syncIframeObject['type'] !== undefined && + syncIframeObject['link'] !== undefined && + syncIframeObject.type === 'iframe') { return true; } + return false; + }).forEach(function (syncIframeObject) { + syncs.push({ + type: 'iframe', + url: appendGdprParams(syncIframeObject.link, gdprParams) + }); + }); + } + if (syncOptions.pixelEnabled) { + respObject.syncs.filter(function (syncImageObject) { + if (syncImageObject['type'] !== undefined && + syncImageObject['link'] !== undefined && + syncImageObject.type === 'image') { return true; } + return false; + }).forEach(function (syncImageObject) { + syncs.push({ + type: 'image', + url: appendGdprParams(syncImageObject.link, gdprParams) + }); + }); + } + } + }); + } + }); + + return syncs; + }, + +} + +registerBidder(spec); diff --git a/modules/mediaimpactBidAdapter.md b/modules/mediaimpactBidAdapter.md new file mode 100644 index 00000000000..fb1a3ea27b2 --- /dev/null +++ b/modules/mediaimpactBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +Module Name: MEDIAIMPACT Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: Info@mediaimpact.com.ua + +# Description + +You can use this adapter to get a bid from mediaimpact.com.ua. + +About us : https://mediaimpact.com.ua + + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'div-ad-example', + sizes: [[300, 250]], + bids: [ + { + bidder: "mediaimpact", + params: { + unitId: 6698 + } + } + ] + }, + { + code: 'div-ad-example-2', + sizes: [[300, 250]], + bids: [ + { + bidder: "mediaimpact", + params: { + partnerId: 6698, + sizes: [[300, 600]], + } + } + ] + } + ]; +``` diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 7af43a3c549..f4967fed170 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -119,7 +119,7 @@ function getOS() { * * @param {*} bid a Prebid.js bid (request) object * @param {string} mediaType the mediaType or the wildcard '*' - * @param {string|array} size the size array or the wildcard '*' + * @param {string|Array} size the size array or the wildcard '*' * @returns {number|boolean} */ function getFloor(bid, mediaType, size = '*') { diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index b9e00f45df9..6a8a35dbfd4 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, deepAccess, - getGptSlotInfoForAdUnitCode, getWindowTop, isArray, isEmpty, @@ -18,9 +17,18 @@ import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ const BIDDER_CODE = 'medianet'; +const TRUSTEDSTACK_CODE = 'trustedstack'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; +const TRUSTEDSTACK_URL = 'https://prebid.trustedstack.com/rtb/trustedstack'; const PLAYER_URL = 'https://prebid.media.net/video/bundle.js'; const SLOT_VISIBILITY = { NOT_DETERMINED: 0, @@ -49,7 +57,7 @@ mnData.urlData = { }; const aliases = [ - { code: 'aax', gvlid: 720 }, + { code: TRUSTEDSTACK_CODE }, ]; getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; @@ -323,8 +331,9 @@ function normalizeCoordinates(coordinates) { } } -function getBidderURL(cid) { - return BID_URL + '?cid=' + encodeURIComponent(cid); +function getBidderURL(bidderCode, cid) { + const url = (bidderCode === TRUSTEDSTACK_CODE) ? TRUSTEDSTACK_URL : BID_URL; + return url + '?cid=' + encodeURIComponent(cid); } function generatePayload(bidRequests, bidderRequests) { @@ -463,7 +472,7 @@ export const spec = { let payload = generatePayload(bidRequests, bidderRequests); return { method: 'POST', - url: getBidderURL(payload.ext.customer_id), + url: getBidderURL(bidderRequests.bidderCode, payload.ext.customer_id), data: JSON.stringify(payload) }; }, diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js index 378a804487a..5cf0ceaba18 100644 --- a/modules/mediasniperBidAdapter.js +++ b/modules/mediasniperBidAdapter.js @@ -1,8 +1,7 @@ import { deepAccess, deepClone, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, inIframe, isArray, isEmpty, @@ -242,7 +241,7 @@ function createImp(bid) { * * @param {*} bid a Prebid.js bid (request) object * @param {string} mediaType the mediaType or the wildcard '*' - * @param {string|array} size the size array or the wildcard '*' + * @param {string|Array} size the size array or the wildcard '*' * @returns {number|boolean} */ function getFloor(bid, mediaType, size = '*') { diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 25b8c509477..a84c19b786b 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -6,6 +6,15 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {Renderer} from '../src/Renderer.js'; import { getRefererInfo } from '../src/refererDetection.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'mediasquare'; const BIDDER_URL_PROD = 'https://pbs-front.mediasquare.fr/' const BIDDER_URL_TEST = 'https://bidder-test.mediasquare.fr/' @@ -20,20 +29,20 @@ export const spec = { aliases: ['msq'], // short code supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { return !!(bid.params.owner && bid.params.code); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -53,6 +62,8 @@ export const spec = { if (tmpFloor != {}) { floor[value.join('x')] = tmpFloor; } }); } + let tmpFloor = adunitValue.getFloor({currency: 'USD', mediaType: '*', size: '*'}); + if (tmpFloor != {}) { floor['*'] = tmpFloor; } } codes.push({ owner: adunitValue.params.owner, @@ -86,6 +97,7 @@ export const spec = { } else if (bidderRequest.hasOwnProperty('bids') && typeof bidderRequest.bids == 'object' && bidderRequest.bids.length > 0 && bidderRequest.bids[0].hasOwnProperty('userId')) { payload.userId = bidderRequest.bids[0].userId; } + if (bidderRequest.ortb2?.regs?.ext?.dsa) { payload.dsa = bidderRequest.ortb2.regs.ext.dsa } }; if (test) { payload.debug = true; } const payloadString = JSON.stringify(payload); @@ -96,11 +108,11 @@ export const spec = { }; }, /** - * 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. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { const serverBody = serverResponse.body; // const headerValue = serverResponse.headers.get('some-response-header'); @@ -125,7 +137,8 @@ export const spec = { 'advertiserDomains': value['adomain'] } }; - let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment']; + if ('dsa' in value) { bidResponse.meta.dsa = value['dsa']; } + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; paramsToSearchFor.forEach(param => { if (param in value) { bidResponse['mediasquare'][param] = value[param]; @@ -148,12 +161,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { @@ -164,9 +177,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction - */ + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid if (bid.hasOwnProperty('mediaType') && bid.mediaType == 'video') { @@ -174,11 +187,14 @@ export const spec = { } let params = { pbjs: '$prebid.version$', referer: encodeURIComponent(getRefererInfo().page || getRefererInfo().topmostLocation) }; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment']; + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; if (bid.hasOwnProperty('mediasquare')) { paramsToSearchFor.forEach(param => { if (bid['mediasquare'].hasOwnProperty(param)) { params[param] = bid['mediasquare'][param]; + if (typeof params[param] == 'number') { + params[param] = params[param].toString(); + } } }); }; diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index fc77c7cc97d..3f3a90c3c49 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -11,6 +11,13 @@ import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'merkleId'; const ID_URL = 'https://prebid.sv.rkdms.com/identity/'; const DEFAULT_REFRESH = 7 * 3600; @@ -87,17 +94,17 @@ function generateId(configParams, configStorage) { /** @type {Submodule} */ export const merkleIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{eids:arrayofields}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{eids:arrayofields}} + */ decode(value) { // Legacy support for a single id const id = (value && value.pam_id && typeof value.pam_id.id === 'string') ? value.pam_id : undefined; @@ -115,12 +122,12 @@ export const merkleIdSubmodule = { }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ getId(config, consentData) { logInfo('User ID - merkleId generating id'); diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 8e889261e52..fb3990e97f1 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -9,11 +9,10 @@ import { isEmpty, triggerPixel, logWarn, - getBidIdParameter, isFn, isNumber, isBoolean, - isInteger, deepSetValue, + isInteger, deepSetValue, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; @@ -22,6 +21,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {USERSYNC_DEFAULT_CONFIG} from '../src/userSync.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const GVLID = 358; const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; diff --git a/modules/mgidRtdProvider.js b/modules/mgidRtdProvider.js index fd2c0bbe6fd..059be4e9103 100644 --- a/modules/mgidRtdProvider.js +++ b/modules/mgidRtdProvider.js @@ -5,6 +5,10 @@ import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'mgid'; const MGID_RTD_API_URL = 'https://servicer.mgid.com/sda'; diff --git a/modules/mgidXBidAdapter.js b/modules/mgidXBidAdapter.js index 5789f0d8b95..ac25a419de1 100644 --- a/modules/mgidXBidAdapter.js +++ b/modules/mgidXBidAdapter.js @@ -17,7 +17,7 @@ import { USERSYNC_DEFAULT_CONFIG } from '../src/userSync.js'; const BIDDER_CODE = 'mgidX'; const GVLID = 358; -const AD_URL = 'https://us-east-x.mgid.com/pbjs'; +const AD_URL = 'https://#{REGION}#.mgid.com/pbjs'; const PIXEL_SYNC_URL = 'https://cm.mgid.com/i.gif'; const IFRAME_SYNC_URL = 'https://cm.mgid.com/i.html'; @@ -166,19 +166,33 @@ export const spec = { placements, coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, tmax: config.getConfig('bidderTimeout') }; + if (bidderRequest.gdprConsent) { + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; placements.push(getPlacementReqData(bid)); } + const region = validBidRequests[0].params?.region; + + let url; + if (region === 'eu') { + url = AD_URL.replace('#{REGION}#', 'eu'); + } else { + url = AD_URL.replace('#{REGION}#', 'us-east-x'); + } + return { method: 'POST', - url: AD_URL, + url: url, data: request }; }, diff --git a/modules/microadBidAdapter.js b/modules/microadBidAdapter.js index ed88dce757c..61aa9b795de 100644 --- a/modules/microadBidAdapter.js +++ b/modules/microadBidAdapter.js @@ -115,6 +115,26 @@ export const spec = { params['aids'] = JSON.stringify(aidsParams) } + const pbadslot = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || pbadslot; + if (gpid) { + params['gpid'] = gpid; + } + + if (pbadslot) { + params['pbadslot'] = pbadslot; + } + + const adservname = deepAccess(bid, 'ortb2Imp.ext.data.adserver.name'); + if (adservname) { + params['adservname'] = adservname; + } + + const adservadslot = deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot'); + if (adservadslot) { + params['adservadslot'] = adservadslot; + } + requests.push({ method: 'GET', url: ENDPOINT_URLS[ENVIRONMENT], diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index bb0bb76bdbc..81200f28a6f 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -7,7 +19,7 @@ const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'minutemedia'; const ADAPTER_VERSION = '6.0.0'; const TTL = 360; -const CURRENCY = 'USD'; +const DEFAULT_CURRENCY = 'USD'; const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/'; const MODES = { PRODUCTION: 'hb-mm-multi', @@ -60,7 +72,7 @@ export const spec = { const bidResponse = { requestId: adUnit.requestId, cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, + currency: adUnit.currency || DEFAULT_CURRENCY, width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, @@ -129,16 +141,16 @@ registerBidder(spec); * @param bid {bid} * @returns {Number} */ -function getFloor(bid, mediaType) { +function getFloor(bid, mediaType, currency) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ - currency: CURRENCY, + currency: currency, mediaType: mediaType, size: '*' }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; } /** @@ -274,6 +286,7 @@ function generateBidParameters(bid, bidderRequest) { const {params} = bid; const mediaType = isBanner(bid) ? BANNER : VIDEO; const sizesArray = getSizesArray(bid, mediaType); + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; // fix floor price in case of NAN if (isNaN(params.floorPrice)) { @@ -284,12 +297,13 @@ function generateBidParameters(bid, bidderRequest) { mediaType, adUnitCode: getBidIdParameter('adUnitCode', bid), sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), bidId: getBidIdParameter('bidId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), transactionId: bid.ortb2Imp?.ext?.tid || '', - coppa: 0 + coppa: 0, }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); @@ -446,6 +460,14 @@ function generateGeneralParams(generalObject, bidderRequest) { generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; } + if (bidderRequest.gppConsent) { + generalParams.gpp = bidderRequest.gppConsent.gppString; + generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + generalParams.gpp = bidderRequest.ortb2.regs.gpp; + generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + if (generalBidParams.ifa) { generalParams.ifa = generalBidParams.ifa; } diff --git a/modules/minutemediaBidAdapter.md b/modules/minutemediaBidAdapter.md index 66b54adaf0e..fdfdf1b32bf 100644 --- a/modules/minutemediaBidAdapter.md +++ b/modules/minutemediaBidAdapter.md @@ -24,6 +24,7 @@ The adapter supports Video(instream) & Banner. | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 | `placementId` | optional | String | A unique placement identifier | "12345678" | `testMode` | optional | Boolean | This activates the test mode | false +| `currency` | optional | String | 3 letters currency | "EUR" # Test Parameters ```javascript diff --git a/modules/minutemediaplusBidAdapter.js b/modules/minutemediaplusBidAdapter.js index 33f3634e17f..146d437b1fa 100644 --- a/modules/minutemediaplusBidAdapter.js +++ b/modules/minutemediaplusBidAdapter.js @@ -252,13 +252,20 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 33fa6857e85..99cad1c7bc6 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -1,12 +1,48 @@ -import { buildUrl, formatQS, logInfo, triggerPixel } from '../src/utils.js'; +import { + buildUrl, + formatQS, + generateUUID, + isFn, + logInfo, + safeJSONParse, + triggerPixel, +} from '../src/utils.js'; +import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'missena'; const ENDPOINT_URL = 'https://bid.missena.io/'; const EVENTS_DOMAIN = 'events.missena.io'; const EVENTS_DOMAIN_DEV = 'events.staging.missena.xyz'; +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); +window.msna_ik = window.msna_ik || generateUUID(); + +/* Get Floor price information */ +function getFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return {}; + } + + const bidFloors = bidRequest.getFloor({ + currency: 'USD', + mediaType: BANNER, + }); + + if (!isNaN(bidFloors.floor)) { + return bidFloors; + } +} + export const spec = { aliases: ['msna'], code: BIDDER_CODE, @@ -30,9 +66,22 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + const capKey = `missena.missena.capper.remove-bubble.${validBidRequests[0]?.params.apiKey}`; + const capping = safeJSONParse(storage.getDataFromLocalStorage(capKey)); + const referer = bidderRequest?.refererInfo?.topmostLocation; + if ( + typeof capping?.expiry === 'number' && + new Date().getTime() < capping?.expiry && + (!capping?.referer || capping?.referer == referer) + ) { + logInfo('Missena - Capped'); + return []; + } + return validBidRequests.map((bidRequest) => { const payload = { adunit: bidRequest.adUnitCode, + ik: window.msna_ik, request_id: bidRequest.bidId, timeout: bidderRequest.timeout, }; @@ -60,7 +109,17 @@ export const spec = { if (bidRequest.params.isInternal) { payload.is_internal = bidRequest.params.isInternal; } + if (bidRequest.ortb2?.device?.ext?.cdep) { + payload.cdep = bidRequest.ortb2?.device?.ext?.cdep; + } payload.userEids = bidRequest.userIdAsEids || []; + payload.version = '$prebid.version$'; + + const bidFloor = getFloor(bidRequest); + payload.floor = bidFloor?.floor; + payload.floor_currency = bidFloor?.currency; + payload.currency = config.getConfig('currency.adServerCurrency') || 'EUR'; + return { method: 'POST', url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), @@ -89,7 +148,7 @@ export const spec = { syncOptions, serverResponses, gdprConsent, - uspConsent + uspConsent, ) { if (!syncOptions.iframeEnabled) { return []; @@ -128,8 +187,13 @@ export const spec = { protocol: 'https', hostname, pathname: '/v1/bidsuccess', - search: { t: bid.params[0].apiKey, provider: bid.meta?.networkName, cpm: bid.cpm, currency: bid.currency }, - }) + search: { + t: bid.params[0].apiKey, + provider: bid.meta?.networkName, + cpm: bid.originalCpm, + currency: bid.originalCurrency, + }, + }), ); logInfo('Missena - Bid won', bid); }, diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js index 9ff50e2531f..35e9b03c031 100644 --- a/modules/mobfoxpbBidAdapter.js +++ b/modules/mobfoxpbBidAdapter.js @@ -71,6 +71,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/multibid/index.js b/modules/multibid/index.js index df77a157bee..27b88d47cf7 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -45,10 +45,10 @@ config.getConfig(MODULE_NAME, conf => { }); /** - * @summary validates multibid configuration entries - * @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] - * @return {Boolean} -*/ + * @summary validates multibid configuration entries + * @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] + * @return {Boolean} + */ export function validateMultibid(conf) { let check = true; let duplicate = conf.filter(entry => { @@ -77,10 +77,10 @@ export function validateMultibid(conf) { } /** - * @summary addBidderRequests before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {Object[]} array containing copy of each bidderRequest object -*/ + * @summary addBidderRequests before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {Object[]} array containing copy of each bidderRequest object + */ export function adjustBidderRequestsHook(fn, bidderRequests) { bidderRequests.map(bidRequest => { // Loop through bidderRequests and check if bidderCode exists in multiconfig @@ -95,11 +95,11 @@ export function adjustBidderRequestsHook(fn, bidderRequests) { } /** - * @summary addBidResponse before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {String} ad unit code for bid - * @param {Object} bid object -*/ + * @summary addBidResponse before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {String} ad unit code for bid + * @param {Object} bid object + */ export const addBidResponseHook = timedBidResponseHook('multibid', function addBidResponseHook(fn, adUnitCode, bid, reject) { let floor = deepAccess(bid, 'floorData.floorValue'); @@ -146,9 +146,9 @@ export const addBidResponseHook = timedBidResponseHook('multibid', function addB }); /** -* A descending sort function that will sort the list of objects based on the following: -* - bids without dynamic aliases are sorted before bids with dynamic aliases -*/ + * A descending sort function that will sort the list of objects based on the following: + * - bids without dynamic aliases are sorted before bids with dynamic aliases + */ export function sortByMultibid(a, b) { if (a.bidder !== a.bidderCode && b.bidder === b.bidderCode) { return 1; @@ -162,13 +162,13 @@ export function sortByMultibid(a, b) { } /** - * @summary getHighestCpmBidsFromBidPool before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {Object[]} array of objects containing all bids from bid pool - * @param {Function} function to reduce to only highest cpm value for each bidderCode - * @param {Number} adUnit bidder targeting limit, default set to 0 - * @param {Boolean} default set to false, this hook modifies targeting and sets to true -*/ + * @summary getHighestCpmBidsFromBidPool before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {Object[]} array of objects containing all bids from bid pool + * @param {Function} function to reduce to only highest cpm value for each bidderCode + * @param {Number} adUnit bidder targeting limit, default set to 0 + * @param {Boolean} default set to false, this hook modifies targeting and sets to true + */ export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { if (!config.getConfig('multibid')) resetMultiConfig(); if (hasMultibid) { @@ -216,18 +216,18 @@ export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBi } /** -* Resets globally stored multibid configuration -*/ + * Resets globally stored multibid configuration + */ export const resetMultiConfig = () => { hasMultibid = false; multiConfig = {}; }; /** -* Resets globally stored multibid ad unit bids -*/ + * Resets globally stored multibid ad unit bids + */ export const resetMultibidUnits = () => multibidUnits = {}; /** -* Set up hooks on init -*/ + * Set up hooks on init + */ function init() { // TODO: does this reset logic make sense - what about simultaneous auctions? events.on(CONSTANTS.EVENTS.AUCTION_INIT, resetMultibidUnits); diff --git a/modules/mwOpenLinkIdSystem.js b/modules/mwOpenLinkIdSystem.js index ff23547224b..c06f61ff82f 100644 --- a/modules/mwOpenLinkIdSystem.js +++ b/modules/mwOpenLinkIdSystem.js @@ -11,6 +11,11 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams + */ + const openLinkID = { name: 'mwol', cookie_expiration: (86400 * 1000 * 365 * 1) // 1 year @@ -112,27 +117,27 @@ export { writeCookie }; /** @type {Submodule} */ export const mwOpenLinkIdSubModule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: 'mwOpenLinkId', /** - * decode the stored id value for passing to bid requests - * @function - * @param {MwOlId} mwOlId - * @return {(Object|undefined} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {MwOlId} mwOlId + * @return {(Object|undefined} + */ decode(mwOlId) { const id = mwOlId && isPlainObject(mwOlId) ? mwOlId.eid : undefined; return id ? { 'mwOpenLinkId': id } : undefined; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleParams} [submoduleParams] - * @returns {id:MwOlId | undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [submoduleParams] + * @returns {id:MwOlId | undefined} + */ getId(submoduleConfig) { const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; if (!isValidConfig(submoduleConfigParams)) return undefined; diff --git a/modules/mygaruIdSystem.js b/modules/mygaruIdSystem.js new file mode 100644 index 00000000000..9133480477b --- /dev/null +++ b/modules/mygaruIdSystem.js @@ -0,0 +1,104 @@ +/** + * This module adds MyGaru Real Time User Sync to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/mygaruIdSystem + * @requires module:modules/userId + */ + +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + +const bidderCode = 'mygaruId'; +const syncUrl = 'https://ident.mygaru.com/v2/id'; + +export function buildUrl(opts) { + const queryPairs = []; + for (let key in opts) { + if (opts[key] !== undefined) { + queryPairs.push(`${key}=${encodeURIComponent(opts[key])}`); + } + } + return `${syncUrl}?${queryPairs.join('&')}`; +} + +function requestRemoteIdAsync(url) { + return new Promise((resolve) => { + ajax( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { iuid } = jsonResponse; + resolve(iuid); + } catch (e) { + resolve(); + } + }, + error: () => { + resolve(); + }, + }, + undefined, + { + method: 'GET', + contentType: 'application/json' + } + ); + }); +} + +/** @type {Submodule} */ +export const mygaruIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: bidderCode, + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{id: string} | null} + */ + decode(id) { + return id; + }, + /** + * get the MyGaru Id from local storages and initiate a new user sync + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {{id: string | undefined}} + */ + getId(config, consentData) { + const gdprApplies = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies ? 1 : 0; + const gdprConsentString = gdprApplies ? consentData.consentString : undefined; + const url = buildUrl({ + gdprApplies, + gdprConsentString + }); + + return { + url, + callback: function (done) { + return requestRemoteIdAsync(url).then((id) => { + done({ mygaruId: id }); + }) + } + } + }, + eids: { + 'mygaruId': { + source: 'mygaru.com', + atype: 1 + }, + } +}; + +submodule('userId', mygaruIdSubmodule); diff --git a/modules/mygaruIdSystem.md b/modules/mygaruIdSystem.md new file mode 100644 index 00000000000..92724f99469 --- /dev/null +++ b/modules/mygaruIdSystem.md @@ -0,0 +1,24 @@ +## Mygaru User ID Submodule + +MyGaru provides single use tokens as a UserId for SSPs and DSP that consume telecom DMP data. + +## Building Prebid with Mygaru ID Support + +First, make sure to add submodule to your Prebid.js package with: + +``` +gulp build --modules=userId,mygaruIdSystem +``` +Params configuration is not required. +Also mygaru is async, in order to get ids for initial ad auctions you need to add auctionDelay param to userSync config. + +```javascript +pbjs.setConfig({ + userSync: { + auctionDelay: 100, + userIds: [{ + name: 'mygaruId', + }] + } +}); +``` diff --git a/modules/nativeRendering.js b/modules/nativeRendering.js new file mode 100644 index 00000000000..8e6b6baab55 --- /dev/null +++ b/modules/nativeRendering.js @@ -0,0 +1,27 @@ +import {getRenderingData} from '../src/adRendering.js'; +import {getNativeRenderingData, isNativeResponse} from '../src/native.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {RENDERER} from '../libraries/creative-renderer-native/renderer.js'; +import {getCreativeRendererSource} from '../src/creativeRenderers.js'; + +function getRenderingDataHook(next, bidResponse, options) { + if (isNativeResponse(bidResponse)) { + next.bail({ + native: getNativeRenderingData(bidResponse, auctionManager.index.getAdUnit(bidResponse)) + }) + } else { + next(bidResponse, options) + } +} +function getRendererSourceHook(next, bidResponse) { + if (isNativeResponse(bidResponse)) { + next.bail(RENDERER); + } else { + next(bidResponse); + } +} + +if (FEATURES.NATIVE) { + getRenderingData.before(getRenderingDataHook) + getCreativeRendererSource.before(getRendererSourceHook); +} diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c62a74e6d6c..69a270247cd 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -2,7 +2,20 @@ import { deepAccess, isEmpty } from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' import { getGlobal } from '../src/prebidGlobal.js' -// import { config } from 'src/config' +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.tagid = bidRequest.adUnitCode + return imp; + } +}); const BIDDER_CODE = 'nativo' const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid' @@ -136,6 +149,10 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + // Get OpenRTB Data + const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest}) + const openRTBDataString = JSON.stringify(openRTBData) + const requestData = new RequestData() requestData.addBidRequestDataSource(new UserEIDs()) @@ -271,8 +288,9 @@ export const spec = { const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings) let serverRequest = { - method: 'GET', - url: requestUrl + method: 'POST', + url: requestUrl, + data: openRTBDataString, } return serverRequest diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 8a472259873..42c6b113566 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -10,6 +10,11 @@ import { ajax } from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'naveggId'; const OLD_NAVEGG_ID = 'nid'; const NAVEGG_ID = 'nvggid'; @@ -74,16 +79,16 @@ function readnavIDFromCookie() { /** @type {Submodule} */ export const naveggIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * decode the stored id value for passing to bid requests - * @function - * @param { Object | string | undefined } value - * @return { Object | string | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { Object | string | undefined } value + * @return { Object | string | undefined } + */ decode(value) { const naveggIdVal = value ? isStr(value) ? value : isPlainObject(value) ? value.id : undefined : undefined; return naveggIdVal ? { @@ -91,11 +96,11 @@ export const naveggIdSubmodule = { } : 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} - */ + * 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 naveggIdString = readnaveggIdFromLocalStorage() || readnaveggIDFromCookie() || getNaveggIdFromApi() || readoldnaveggIDFromCookie() || readnvgIDFromCookie() || readnavIDFromCookie(); diff --git a/modules/netIdSystem.js b/modules/netIdSystem.js index 6f1ffe8b0e7..4765f892a97 100644 --- a/modules/netIdSystem.js +++ b/modules/netIdSystem.js @@ -7,6 +7,13 @@ import {submodule} from '../src/hook.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + /** @type {Submodule} */ export const netIdSubmodule = { /** diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 00a3c59b4a6..7c594e2a1c3 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -10,14 +10,14 @@ const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2 const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1' const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2' -function init(config = {}, userConsent) { - config.params = config.params || {} +function init(config, userConsent) { + // config.params = config.params || {} // ignore module if publicToken is missing (module setup failure) - if (!config.params.publicToken) { + if (!config || !config.params || !config.params.publicToken) { logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken') return false; } - if (!config.params.apiUrl) { + if (!config || !config.params || !config.params.apiUrl) { logError('apiUrl missing', 'NeuwoRTDModule', 'config.params.apiUrl') return false; } @@ -25,14 +25,14 @@ function init(config = {}, userConsent) { } export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - config.params = config.params || {}; + const confParams = config.params || {}; logInfo('NeuwoRTDModule', 'starting getBidRequestData') - const wrappedArgUrl = encodeURIComponent(config.params.argUrl || getRefererInfo().page); + const wrappedArgUrl = encodeURIComponent(confParams.argUrl || getRefererInfo().page); /* adjust for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') */ - const joiner = config.params.apiUrl.indexOf('?') < 0 ? '?' : '&' - const url = config.params.apiUrl + joiner + [ - 'token=' + config.params.publicToken, + const joiner = confParams.apiUrl.indexOf('?') < 0 ? '?' : '&' + const url = confParams.apiUrl + joiner + [ + 'token=' + confParams.publicToken, 'url=' + wrappedArgUrl ].join('&') const billingId = generateUUID(); @@ -71,7 +71,7 @@ export function addFragment(base, path, addition) { /** * Concatenate a base array and an array within an object * non-array bases will be arrays, non-arrays at object key will be discarded - * @param {array} base base array to add to + * @param {Array} base base array to add to * @param {object} source object to get an array from * @param {string} key dot-notated path to array within object * @returns base + source[key] if that's an array diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index 2adead66d4e..fb52734d451 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -33,6 +33,8 @@ pbjs.setConfig({realTimeData: { dataProviders: [ neuwoDataProvider ]}}) # Testing +`gulp test --modules=rtdModule,neuwoRtdProvider` + ## Add development tools if necessary - Install node for npm diff --git a/modules/newspassid.md b/modules/newspassid.md new file mode 100644 index 00000000000..6fa709e5ba6 --- /dev/null +++ b/modules/newspassid.md @@ -0,0 +1,76 @@ +--- +Module Name: NewspassId Bidder Adapter +Module Type: Bidder Adapter +Maintainer: techsupport@newspassid.com +layout: bidder +title: Newspass ID +description: LMC Newspass ID Prebid JS Bidder Adapter +biddercode: newspassid +gdpr_supported: false +gvl_id: none +usp_supported: true +coppa_supported: false +schain_supported: true +dchain_supported: false +userIds: criteo, id5Id, tdid, identityLink, liveIntentId, parrableId, pubCommonId, lotamePanoramaId, sharedId, fabrickId +media_types: banner +safeframes_ok: true +deals_supported: true +floors_supported: false +fpd_supported: false +pbjs: true +pbs: false +prebid_member: false +multiformat_supported: will-bid-on-any +--- + +### Description + +LMC Newspass ID Prebid JS Bidder Adapter that connects to the NewspassId demand source(s). + +The Newspass bid adapter supports Banner mediaTypes ONLY. +This is intended for USA audiences only, and does not support GDPR + + +### Bid Params + +{: .table .table-bordered .table-striped } + +| Name | Scope | Description | Example | Type | +|-----------|----------|---------------------------|------------|----------| +| `siteId` | required | The site ID. | `"NPID0000001"` | `string` | +| `publisherId` | required | The publisher ID. | `"4204204201"` | `string` | +| `placementId` | required | The placement ID. | `"0420420421"` | `string` | +| `customData` | optional | publisher key-values used for targeting | `[{"settings":{},"targeting":{"key1": "value1", "key2": "value2"}}], ` | `array` | + +### Test Parameters + + +A test ad unit that will consistently return test creatives: + +``` + +//Banner adUnit + +adUnits = [{ + code: 'id-of-your-banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'newspassid', + params: { + publisherId: 'NEWSPASS0001', /* an ID to identify the publisher account - required */ + siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ + placementId: '8000000015', /* an ID used to identify the piece of inventory - required - for appnexus test use 13144370. */ + customData: [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"]}}],/* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ + } + }] + }]; +``` + +### Note: + +Please contact us at techsupport@newspassid.com for any assistance testing your implementation before going live into production. diff --git a/modules/newspassidBidAdapter.js b/modules/newspassidBidAdapter.js index b440edc2beb..2a4b2da186b 100644 --- a/modules/newspassidBidAdapter.js +++ b/modules/newspassidBidAdapter.js @@ -1,15 +1,24 @@ -import {contains, deepAccess, deepSetValue, isArray, logError, logInfo, logWarn, parseUrl} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import { + logInfo, + logError, + deepAccess, + logWarn, + deepSetValue, + isArray, + contains, + parseUrl, + generateUUID +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {getPriceBucketString} from '../src/cpmBucketManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; - const BIDDER_CODE = 'newspassid'; const ORIGIN = 'https://bidder.newspassid.com' // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const NEWSPASSCOOKIESYNC = '/static/load-cookie.html'; -const NEWSPASSVERSION = '1.0.1'; +const NEWSPASSVERSION = '1.1.4'; export const spec = { version: NEWSPASSVERSION, code: BIDDER_CODE, @@ -155,7 +164,7 @@ export const spec = { let placementId = placementIdOverrideFromGetParam || this.getPlacementId(npBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = npBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder newspass made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().page); obj.secure = parsed.protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!npBidRequest.hasOwnProperty('mediaTypes')) { @@ -189,7 +198,6 @@ export const spec = { deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); obj.ext['newspassid'] = {}; obj.ext['newspassid'].adUnitCode = npBidRequest.adUnitCode; // eg. 'mpu' - obj.ext['newspassid'].transactionId = npBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (npBidRequest.params.hasOwnProperty('customData')) { obj.ext['newspassid'].customData = npBidRequest.params.customData; } @@ -216,6 +224,10 @@ export const spec = { if (!schain && deepAccess(npBidRequest, 'schain')) { schain = npBidRequest.schain; } + let gpid = deepAccess(npBidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + deepSetValue(obj, 'ext.gpid', gpid); + } return obj; }); let extObj = {}; @@ -240,7 +252,7 @@ export const spec = { let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module npRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': getRefererInfo().page, + 'page': this.getRefererInfo().page, 'id': htmlParams.siteId }; npRequest.test = config.getConfig('debug') ? 1 : 0; @@ -259,12 +271,9 @@ export const spec = { } if (singleRequest) { logInfo('buildRequests starting to generate response for a single request'); - // TODO: fix auctionId & transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - npRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. - npRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + npRequest.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) npRequest.imp = tosendtags; npRequest.ext = extObj; - deepSetValue(npRequest, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(npRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', @@ -280,12 +289,9 @@ export const spec = { let arrRet = tosendtags.map(imp => { logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let npRequestSingle = Object.assign({}, npRequest); - imp.ext['newspassid'].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - npRequestSingle.id = imp.ext['newspassid'].transactionId; // Unique ID of the bid request, provided by the exchange. - npRequestSingle.auctionId = imp.ext['newspassid'].transactionId; // not sure if this should be here? + npRequestSingle.id = generateUUID(); npRequestSingle.imp = [imp]; npRequestSingle.ext = extObj; - deepSetValue(npRequestSingle, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(npRequestSingle, 'user.ext.eids', userExtEids); logInfo('buildRequests RequestSingle (for non-single) = ', npRequestSingle); return { @@ -305,6 +311,7 @@ export const spec = { logInfo(`interpretResponse time: ${startTime}. buildRequests done -> interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; + let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { return []; } @@ -351,7 +358,7 @@ export const spec = { logInfo(`newspassid.enhancedAdserverTargeting is set to false, no per-bid keys will be sent to adserver.`); } let {seat: winningSeat, bid: winningBid} = this.getWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting['np_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting['np_auc_id'] = String(aucId); adserverTargeting['np_winner'] = String(winningSeat); adserverTargeting['np_bid'] = 'true'; if (enhancedAdserverTargeting) { @@ -490,10 +497,29 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().location); // was getRefererInfo().page but this is not backwards compatible logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, + getRefererInfo() { + if (getRefererInfo().hasOwnProperty('location')) { + logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); + return getRefererInfo(); + } else { + logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); + try { + return { + page: top.location.href, + location: top.location.href + }; + } catch (e) { + return { + page: window.location.href, + location: window.location.href + }; + } + } + }, blockTheRequest() { let npRequest = config.getConfig('newspassid.np_request'); if (typeof npRequest == 'boolean' && !npRequest) { diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index cb660ad9fd6..de91b508125 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -2,45 +2,70 @@ import { _each, createTrackPixelHtml, deepAccess, + deepSetValue, getBidIdParameter, getDefinedParams, getWindowTop, isArray, isStr, - logMessage, parseGPTSingleSizeArrayToRtbSize, parseUrl, triggerPixel, } from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; import CONSTANTS from '../src/constants.json'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -import * as events from '../src/events.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; +const NM_VERSION = '3.1.0'; +const GVLID = 1060; const BIDDER_CODE = 'nextMillennium'; const ENDPOINT = 'https://pbs.nextmillmedia.com/openrtb2/auction'; const TEST_ENDPOINT = 'https://test.pbs.nextmillmedia.com/openrtb2/auction'; -const SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?'; +const SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}'; const REPORT_ENDPOINT = 'https://report2.hb.brainlyads.com/statistics/metric'; const TIME_TO_LIVE = 360; -const VIDEO_PARAMS = [ - 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', - 'playbackmethod', 'protocols', 'startdelay' -]; -const GVLID = 1060; - -const sendingDataStatistic = initSendingDataStatistic(); -events.on(CONSTANTS.EVENTS.AUCTION_INIT, auctionInitHandler); - -const EXPIRENCE_WURL = 20 * 60000; -const wurlMap = {}; -cleanWurl(); +const DEFAULT_CURRENCY = 'USD'; + +const VIDEO_PARAMS_DEFAULT = { + api: undefined, + context: undefined, + delivery: undefined, + linearity: undefined, + maxduration: undefined, + mimes: [ + 'video/mp4', + 'video/x-ms-wmv', + 'application/javascript', + ], + + minduration: undefined, + placement: undefined, + plcmt: undefined, + playbackend: undefined, + playbackmethod: undefined, + pos: undefined, + protocols: undefined, + skip: undefined, + skipafter: undefined, + skipmin: undefined, + startdelay: undefined, +}; -events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); +const VIDEO_PARAMS = Object.keys(VIDEO_PARAMS_DEFAULT); +const ALLOWED_ORTB2_PARAMETERS = [ + 'site.pagecat', + 'site.content.cat', + 'site.content.language', + 'device.sua', + 'site.keywords', + 'site.content.keywords', + 'user.keywords', +]; export const spec = { code: BIDDER_CODE, @@ -57,90 +82,44 @@ export const spec = { const requests = []; window.nmmRefreshCounts = window.nmmRefreshCounts || {}; - _each(validBidRequests, function(bid) { + _each(validBidRequests, (bid) => { window.nmmRefreshCounts[bid.adUnitCode] = window.nmmRefreshCounts[bid.adUnitCode] || 0; const id = getPlacementId(bid); const auctionId = bid.auctionId; const bidId = bid.bidId; - let sizes = bid.sizes; - if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; const site = getSiteObj(); const device = getDeviceObj(); + const {cur, mediaTypes} = getCurrency(bid); const postBody = { - 'id': bidderRequest?.bidderRequestId, - 'ext': { - 'prebid': { - 'storedrequest': { - 'id': id - } + id: bidderRequest?.bidderRequestId, + cur, + ext: { + prebid: { + storedrequest: { + id, + }, }, - 'nextMillennium': { - 'refresh_count': window.nmmRefreshCounts[bid.adUnitCode]++, - 'elOffsets': getBoundingClient(bid), - 'scrollTop': window.pageYOffset || document.documentElement.scrollTop - } + nextMillennium: { + nm_version: NM_VERSION, + pbjs_version: getGlobal()?.version || undefined, + refresh_count: window.nmmRefreshCounts[bid.adUnitCode]++, + elOffsets: getBoundingClient(bid), + scrollTop: window.pageYOffset || document.documentElement.scrollTop, + }, }, device, site, - imp: [] + imp: [], }; - const imp = { - id: bid.adUnitCode, - ext: { - prebid: { - storedrequest: {id} - } - } - }; - - if (deepAccess(bid, 'mediaTypes.banner')) { - imp.banner = { - format: (sizes || []).map(s => { return {w: s[0], h: s[1]} }) - }; - }; - - const video = deepAccess(bid, 'mediaTypes.video'); - if (video) { - imp.video = getDefinedParams(video, VIDEO_PARAMS); - if (video.playerSize) { - imp.video = Object.assign( - imp.video, parseGPTSingleSizeArrayToRtbSize(video.playerSize[0]) || {} - ); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; - }; - }; - - postBody.imp.push(imp); - - const gdprConsent = bidderRequest && bidderRequest.gdprConsent; - const uspConsent = bidderRequest && bidderRequest.uspConsent; - - if (gdprConsent || uspConsent) { - postBody.regs = { ext: {} }; - - if (uspConsent) { - postBody.regs.ext.us_privacy = uspConsent; - }; - - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies !== 'undefined') { - postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; - }; - - if (typeof gdprConsent.consentString !== 'undefined') { - postBody.user = { - ext: { consent: gdprConsent.consentString } - }; - }; - }; - }; + postBody.imp.push(getImp(bid, id, mediaTypes)); + setConsentStrings(postBody, bidderRequest); + setOrtb2Parameters(postBody, bidderRequest?.ortb2); + setEids(postBody, bid); const urlParameters = parseUrl(getWindowTop().location.href).search; const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; @@ -152,13 +131,15 @@ export const spec = { data: JSON.stringify(postBody), options: { contentType: 'text/plain', - withCredentials: true + withCredentials: true, }, bidId, params, auctionId, }); + + this.getUrlPixelMetric(CONSTANTS.EVENTS.BID_REQUESTED, bid); }); return requests; @@ -172,10 +153,6 @@ export const spec = { _each(resp.bid, (bid) => { const requestId = bidRequest.bidId; const params = bidRequest.params; - const auctionId = bidRequest.auctionId; - const wurl = deepAccess(bid, 'ext.prebid.events.win'); - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - addWurl({auctionId, requestId, wurl}); const {ad, adUrl, vastUrl, vastXml} = getAd(bid); @@ -186,12 +163,12 @@ export const spec = { width: bid.w, height: bid.h, creativeId: bid.adid, - currency: response.cur, + currency: response.cur || DEFAULT_CURRENCY, netRevenue: true, ttl: TIME_TO_LIVE, meta: { - advertiserDomains: bid.adomain || [] - } + advertiserDomains: bid.adomain || [], + }, }; if (vastUrl || vastXml) { @@ -205,48 +182,51 @@ export const spec = { }; bidResponses.push(bidResponse); + + this.getUrlPixelMetric(CONSTANTS.EVENTS.BID_RESPONSE, bid); }); }); return bidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) return []; + const pixels = []; + const getSetPixelFunc = type => url => { pixels.push({type, url: replaceUsersyncMacros(url, gdprConsent, uspConsent, gppConsent, type)}) }; + const getSetPixelsFunc = type => response => { deepAccess(response, `body.ext.sync.${type}`, []).forEach(getSetPixelFunc(type)) }; + + const setPixel = (type, url) => { (getSetPixelFunc(type))(url) }; + const setPixelImages = getSetPixelsFunc('image'); + const setPixelIframes = getSetPixelsFunc('iframe'); if (isArray(responses)) { responses.forEach(response => { - if (syncOptions.pixelEnabled) { - deepAccess(response, 'body.ext.sync.image', []).forEach(imgUrl => { - pixels.push({ - type: 'image', - url: replaceUsersyncMacros(imgUrl, gdprConsent, uspConsent) - }); - }) - } - - if (syncOptions.iframeEnabled) { - deepAccess(response, 'body.ext.sync.iframe', []).forEach(iframeUrl => { - pixels.push({ - type: 'iframe', - url: replaceUsersyncMacros(iframeUrl, gdprConsent, uspConsent) - }); - }) - } - }) + if (syncOptions.pixelEnabled) setPixelImages(response); + if (syncOptions.iframeEnabled) setPixelIframes(response); + }); } if (!pixels.length) { - let syncUrl = SYNC_ENDPOINT; - if (gdprConsent && gdprConsent.gdprApplies) syncUrl += 'gdpr=1&gdpr_consent=' + gdprConsent.consentString + '&'; - if (uspConsent) syncUrl += 'us_privacy=' + uspConsent + '&'; - if (syncOptions.iframeEnabled) pixels.push({type: 'iframe', url: syncUrl + 'type=iframe'}); - if (syncOptions.pixelEnabled) pixels.push({type: 'image', url: syncUrl + 'type=image'}); + if (syncOptions.pixelEnabled) setPixel('image', SYNC_ENDPOINT); + if (syncOptions.iframeEnabled) setPixel('iframe', SYNC_ENDPOINT); } + return pixels; }, getUrlPixelMetric(eventName, bid) { + const disabledSending = !!config.getBidderConfig()?.nextMillennium?.disabledSendingStatisticData; + if (disabledSending) return; + + const url = this._getUrlPixelMetric(eventName, bid); + if (!url) return; + + triggerPixel(url); + }, + + _getUrlPixelMetric(eventName, bid) { const bidder = bid.bidder || bid.bidderCode; if (bidder != BIDDER_CODE) return; @@ -280,29 +260,156 @@ export const spec = { return url; }, + + onTimeout(bids) { + for (const bid of bids) { + this.getUrlPixelMetric(CONSTANTS.EVENTS.BID_TIMEOUT, bid); + }; + }, }; -function replaceUsersyncMacros(url, gdprConsent, uspConsent) { - const { consentString, gdprApplies } = gdprConsent || {}; +export function getImp(bid, id, mediaTypes) { + const {banner, video} = mediaTypes; + const imp = { + id: bid.adUnitCode, + ext: { + prebid: { + storedrequest: { + id, + }, + }, + }, + }; - if (gdprApplies) { - const gdpr = Number(gdprApplies); - url = url.replace('{{.GDPR}}', gdpr); + getImpBanner(imp, banner); + getImpVideo(imp, video); - if (gdpr == 1 && consentString && consentString.length > 0) { - url = url.replace('{{.GDPRConsent}}', consentString); - } - } else { - url = url.replace('{{.GDPR}}', 0); - url = url.replace('{{.GDPRConsent}}', ''); - } + return imp; +}; + +export function getImpBanner(imp, banner) { + if (!banner) return; + + if (banner.bidfloorcur) imp.bidfloorcur = banner.bidfloorcur; + if (banner.bidfloor) imp.bidfloor = banner.bidfloor; + + const format = (banner.data?.sizes || []).map(s => { return {w: s[0], h: s[1]} }) + const {w, h} = (format[0] || {}) + imp.banner = { + w, + h, + format, + }; +}; + +export function getImpVideo(imp, video) { + if (!video) return; + + if (video.bidfloorcur) imp.bidfloorcur = video.bidfloorcur; + if (video.bidfloor) imp.bidfloor = video.bidfloor; + + imp.video = getDefinedParams(video.data, VIDEO_PARAMS); + Object.keys(VIDEO_PARAMS_DEFAULT) + .filter(videoParamName => VIDEO_PARAMS_DEFAULT[videoParamName]) + .forEach(videoParamName => { + if (typeof imp.video[videoParamName] === 'undefined') imp.video[videoParamName] = VIDEO_PARAMS_DEFAULT[videoParamName]; + }); + + if (video.data.playerSize) { + imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(video.data?.playerSize) || {}); + } else if (video.data.w && video.data.h) { + imp.video.w = video.data.w; + imp.video.h = video.data.h; + }; +}; + +export function setConsentStrings(postBody = {}, bidderRequest) { + const gdprConsent = bidderRequest?.gdprConsent; + const uspConsent = bidderRequest?.uspConsent; + let gppConsent = bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent; + if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) gppConsent = bidderRequest?.ortb2?.regs; + + if (gdprConsent || uspConsent || gppConsent) { + postBody.regs = { ext: {} }; + + if (uspConsent) { + postBody.regs.ext.us_privacy = uspConsent; + }; + + if (gppConsent) { + postBody.regs.gpp = gppConsent?.gppString || gppConsent?.gpp; + postBody.regs.gpp_sid = bidderRequest.gppConsent?.applicableSections || gppConsent?.gpp_sid; + }; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + }; + + if (typeof gdprConsent.consentString !== 'undefined') { + postBody.user = { + ext: { consent: gdprConsent.consentString }, + }; + }; + }; + }; +}; - if (uspConsent) { - url = url.replace('{{.USPrivacy}}', uspConsent); +export function setOrtb2Parameters(postBody, ortb2 = {}) { + for (let parameter of ALLOWED_ORTB2_PARAMETERS) { + const value = deepAccess(ortb2, parameter); + if (value) deepSetValue(postBody, parameter, value); } +} + +export function setEids(postBody, bid) { + if (!isArray(bid.userIdAsEids) || !bid.userIdAsEids.length) return; + + deepSetValue(postBody, 'user.eids', bid.userIdAsEids); +} + +export function replaceUsersyncMacros(url, gdprConsent = {}, uspConsent = '', gppConsent = {}, type = '') { + const { consentString = '', gdprApplies = false } = gdprConsent; + const gdpr = Number(gdprApplies); + url = url + .replace('{{.GDPR}}', gdpr) + .replace('{{.GDPRConsent}}', consentString) + .replace('{{.USPrivacy}}', uspConsent) + .replace('{{.GPP}}', gppConsent.gppString || '') + .replace('{{.GPPSID}}', (gppConsent.applicableSections || []).join(',')) + .replace('{{.TYPE_PIXEL}}', type); return url; -}; +} + +function getCurrency(bid = {}) { + const currency = config?.getConfig('currency')?.adServerCurrency || DEFAULT_CURRENCY; + const cur = []; + const types = ['banner', 'video']; + const mediaTypes = {}; + for (const mediaType of types) { + const mediaTypeData = deepAccess(bid, `mediaTypes.${mediaType}`); + if (mediaTypeData) { + mediaTypes[mediaType] = {data: mediaTypeData}; + } else { + continue; + }; + + if (typeof bid.getFloor === 'function') { + let floorInfo = bid.getFloor({currency, mediaType, size: '*'}); + mediaTypes[mediaType].bidfloorcur = floorInfo.currency; + mediaTypes[mediaType].bidfloor = floorInfo.floor; + } else { + mediaTypes[mediaType].bidfloorcur = currency; + }; + + if (cur.includes(mediaTypes[mediaType].bidfloorcur)) cur.push(mediaTypes[mediaType].bidfloorcur); + }; + + if (!cur.length) cur.push(DEFAULT_CURRENCY); + + return {cur, mediaTypes}; +} function getAdEl(bid) { // best way I could think of to get El, is by matching adUnitCode to google slots... @@ -369,7 +476,7 @@ function getAd(bid) { } else if (bid.nurl) { adUrl = bid.nurl; }; - } + }; return {ad, adUrl, vastXml, vastUrl}; } @@ -377,10 +484,21 @@ function getAd(bid) { function getSiteObj() { const refInfo = (getRefererInfo && getRefererInfo()) || {}; + let language = navigator.language; + let content; + if (language) { + // get ISO-639-1-alpha-2 (2 character language) + language = language.split('-')[0]; + content = { + language, + }; + }; + return { page: refInfo.page, ref: refInfo.ref, - domain: refInfo.domain + domain: refInfo.domain, + content, }; } @@ -388,129 +506,20 @@ function getDeviceObj() { return { w: window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth || 0, h: window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight || 0, + ua: window.navigator.userAgent || undefined, + sua: getSua(), }; } -function getKeyWurl({auctionId, requestId}) { - return `${auctionId}-${requestId}`; -} - -function addWurl({wurl, requestId, auctionId}) { - if (!wurl) return; - - const expirence = Date.now() + EXPIRENCE_WURL; - const key = getKeyWurl({auctionId, requestId}); - wurlMap[key] = {wurl, expirence}; -} - -function removeWurl({auctionId, requestId}) { - const key = getKeyWurl({auctionId, requestId}); - delete wurlMap[key]; -} - -function getWurl({auctionId, requestId}) { - const key = getKeyWurl({auctionId, requestId}); - return wurlMap[key] && wurlMap[key].wurl; -} +function getSua() { + let {brands, mobile, platform} = (window?.navigator?.userAgentData || {}); + if (!(brands && platform)) return undefined; -function bidWonHandler(bid) { - const {auctionId, requestId} = bid; - const wurl = getWurl({auctionId, requestId}); - if (wurl) { - logMessage(`(nextmillennium) Invoking image pixel for wurl on BID_WIN: "${wurl}"`); - triggerPixel(wurl); - removeWurl({auctionId, requestId}); - }; -} - -function auctionInitHandler() { - sendingDataStatistic.initEvents(); -} - -function cleanWurl() { - const dateNow = Date.now(); - Object.keys(wurlMap).forEach(key => { - if (dateNow >= wurlMap[key].expirence) { - delete wurlMap[key]; - }; - }); - - setTimeout(cleanWurl, 60000); -} - -function initSendingDataStatistic() { - class SendingDataStatistic { - eventNames = [ - CONSTANTS.EVENTS.BID_TIMEOUT, - CONSTANTS.EVENTS.BID_RESPONSE, - CONSTANTS.EVENTS.BID_REQUESTED, - CONSTANTS.EVENTS.NO_BID, - ]; - - disabledSending = false; - enabledSending = false; - eventHendlers = {}; - - initEvents() { - this.disabledSending = !!config.getBidderConfig()?.nextMillennium?.disabledSendingStatisticData; - if (this.disabledSending) { - this.removeEvents(); - } else { - this.createEvents(); - }; - } - - createEvents() { - if (this.enabledSending) return; - - this.enabledSending = true; - for (let eventName of this.eventNames) { - if (!this.eventHendlers[eventName]) { - this.eventHendlers[eventName] = this.eventHandler(eventName); - }; - - events.on(eventName, this.eventHendlers[eventName]); - }; - } - - removeEvents() { - if (!this.enabledSending) return; - - this.enabledSending = false; - for (let eventName of this.eventNames) { - if (!this.eventHendlers[eventName]) continue; - - events.off(eventName, this.eventHendlers[eventName]); - }; - } - - eventHandler(eventName) { - const eventHandlerFunc = this.getEventHandler(eventName); - if (eventName == CONSTANTS.EVENTS.BID_TIMEOUT) { - return bids => { - if (this.disabledSending || !Array.isArray(bids)) return; - - for (let bid of bids) { - eventHandlerFunc(bid); - }; - } - }; - - return eventHandlerFunc; - } - - getEventHandler(eventName) { - return bid => { - if (this.disabledSending) return; - - const url = spec.getUrlPixelMetric(eventName, bid); - if (!url) return; - triggerPixel(url); - }; - } + return { + brands, + mobile: Number(!!mobile), + platform: (platform && {brand: platform}) || undefined, }; - - return new SendingDataStatistic(); } registerBidder(spec); diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index 0dd4b334f6e..8a41efe4dcc 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,6 +1,5 @@ import { - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isNumber, @@ -15,6 +14,12 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {find} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'nextroll'; const BIDDER_ENDPOINT = 'https://d.adroll.com/bid/prebid/'; const ADAPTER_VERSION = 5; diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index 671cc800980..c31c3d81aeb 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -8,12 +8,21 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js' import { INSTREAM, OUTSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const BIDDER_CODE = 'nexx360'; const REQUEST_URL = 'https://fast.nexx360.io/booster'; const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '2.0'; +const BIDDER_VERSION = '4.0'; const GVLID = 965; const NEXXID_KEY = 'nexx360_storage'; @@ -142,7 +151,6 @@ function isBidRequestValid(bid) { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids * @return ServerRequest Info describing the request to the server. */ @@ -190,10 +198,16 @@ function interpretResponse(serverResponse) { demandSource: bid.ext.ssp, }, }; - if (allowAlternateBidderCodes) response.bidderCode = `n360-${bid.ext.ssp}`; + if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; - if (bid.ext.mediaType === BANNER) response.adUrl = bid.ext.adUrl; - if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.ext.vastXml; + if (bid.ext.mediaType === BANNER) { + if (bid.adm) { + response.ad = bid.adm; + } else { + response.adUrl = bid.ext.adUrl; + } + } + if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.adm; if (bid.ext.mediaType === OUTSTREAM) { response.renderer = createRenderer(bid, OUTSTREAM_RENDERER_URL); diff --git a/modules/nobidAnalyticsAdapter.js b/modules/nobidAnalyticsAdapter.js new file mode 100644 index 00000000000..3a272c3f796 --- /dev/null +++ b/modules/nobidAnalyticsAdapter.js @@ -0,0 +1,257 @@ +import {deepClone, logError, getParameterByName} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; + +const VERSION = '2.0.1'; +const MODULE_NAME = 'nobidAnalyticsAdapter'; +const ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS = 5 * 1000; +const RETENTION_SECONDS = 1 * 24 * 3600; +const TEST_ALLOCATION_PERCENTAGE = 5; // dont block 5% of the time; +window.nobidAnalyticsVersion = VERSION; +const analyticsType = 'endpoint'; +const url = 'localhost:8383/event'; +const GVLID = 816; +const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME, moduleType: MODULE_TYPE_ANALYTICS}); +const { + EVENTS: { + AUCTION_INIT, + BID_REQUESTED, + BID_TIMEOUT, + BID_RESPONSE, + BID_WON, + AUCTION_END, + AD_RENDER_SUCCEEDED + } +} = CONSTANTS; +function log (msg) { + // eslint-disable-next-line no-console + console.log(`%cNoBid Analytics ${VERSION}`, 'padding: 2px 8px 2px 8px; background-color:#f50057; color: white', msg); +} +function isJson (str) { + return str && str.startsWith('{') && str.endsWith('}'); +} +function isExpired (data, retentionSeconds) { + retentionSeconds = retentionSeconds || RETENTION_SECONDS; + if (data.ts + retentionSeconds * 1000 < Date.now()) return true; + return false; +} +function sendEvent (event, eventType) { + function resolveEndpoint() { + var ret = 'https://carbon-nv.servenobids.com/admin/status'; + var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; + if (!env) ret = 'https://carbon-nv.servenobids.com'; + else if (env == 'dev') ret = 'https://localhost:8383'; + return ret; + } + if (!nobidAnalytics.initOptions || !nobidAnalytics.initOptions.siteId || !event) return; + if (nobidAnalytics.isAnalyticsDisabled(eventType)) { + log('NoBid Analytics is Disabled'); + return; + } + try { + event.version = VERSION; + const endpoint = `${resolveEndpoint()}/event/${eventType}?pubid=${nobidAnalytics.initOptions.siteId}`; + ajax(endpoint, + function (response) { + try { + nobidAnalytics.processServerResponse(response); + } catch (e) { + logError(e); + } + }, + JSON.stringify(event), + { + contentType: 'application/json', + method: 'POST' + } + ); + } catch (err) { + log(`Sending event error ${err}`); + } +} +function cleanupObjectAttributes (obj, attributes) { + if (!obj) return; + if (Array.isArray(obj)) { + obj.forEach(item => { + Object.keys(item).forEach(attr => { if (!attributes.includes(attr)) delete item[attr] }); + }); + } else Object.keys(obj).forEach(attr => { if (!attributes.includes(attr)) delete obj[attr] }); +} +function sendBidWonEvent (event, eventType) { + const data = deepClone(event); + cleanupObjectAttributes(data, ['bidderCode', 'size', 'statusMessage', 'adId', 'requestId', 'mediaType', 'adUnitCode', 'cpm', 'currency', 'originalCpm', 'originalCurrency', 'timeToRespond']); + if (nobidAnalytics.topLocation) data.topLocation = nobidAnalytics.topLocation; + sendEvent(data, eventType); +} +function sendAuctionEndEvent (event, eventType) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } + const data = deepClone(event); + + cleanupObjectAttributes(data, ['timestamp', 'timeout', 'auctionId', 'bidderRequests', 'bidsReceived']); + if (data) cleanupObjectAttributes(data.bidderRequests, ['bidderCode', 'bidderRequestId', 'bids', 'refererInfo']); + if (data) cleanupObjectAttributes(data.bidsReceived, ['bidderCode', 'width', 'height', 'adUnitCode', 'statusMessage', 'requestId', 'mediaType', 'cpm', 'currency', 'originalCpm', 'originalCurrency']); + if (data) cleanupObjectAttributes(data.noBids, ['bidder', 'sizes', 'bidId']); + if (data.bidderRequests) { + data.bidderRequests.forEach(bidderRequest => { + cleanupObjectAttributes(bidderRequest.bids, ['mediaTypes', 'adUnitCode', 'sizes', 'bidId']); + }); + } + if (data.bidderRequests) { + data.bidderRequests.forEach(bidderRequest => { + cleanupObjectAttributes(bidderRequest.refererInfo, ['topmostLocation']); + }); + } + sendEvent(data, eventType); +} +function auctionInit (event) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } +} +let nobidAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + auctionInit(args); + break; + case BID_REQUESTED: + break; + case BID_RESPONSE: + break; + case BID_WON: + sendBidWonEvent(args, eventType); + break; + case BID_TIMEOUT: + break; + case AUCTION_END: + sendAuctionEndEvent(args, eventType); + break; + case AD_RENDER_SUCCEEDED: + break; + default: + break; + } + } +}); + +nobidAnalytics = { + ...nobidAnalytics, + originEnableAnalytics: nobidAnalytics.enableAnalytics, // save the base class function + enableAnalytics: function (config) { // override enableAnalytics so we can get access to the config passed in from the page + if (!config.options.siteId) { + logError('NoBid Analytics - siteId parameter is not defined. Analytics won\'t work'); + return; + } + this.initOptions = config.options; + this.originEnableAnalytics(config); // call the base class function + }, + retentionSeconds: RETENTION_SECONDS, + isExpired (data) { + return isExpired(data, this.retentionSeconds); + }, + isAnalyticsDisabled (eventType) { + let stored = storage.getDataFromLocalStorage(this.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (this.isExpired(stored)) return false; + if (stored.disabled === 1) return true; + else if (stored.disabled === 0) return false; + if (eventType) { + if (stored[`disabled_${eventType}`] === 1) return true; + else if (stored[`disabled_${eventType}`] === 0) return false; + } + return false; + }, + processServerResponse (response) { + if (!isJson(response)) return; + const resp = JSON.parse(response); + storage.setDataInLocalStorage(this.ANALYTICS_DATA_NAME, JSON.stringify({ ...resp, ts: Date.now() })); + }, + ANALYTICS_DATA_NAME: 'analytics.nobid.io', + ANALYTICS_OPT_NAME: 'analytics.nobid.io.optData' +} + +adapterManager.registerAnalyticsAdapter({ + adapter: nobidAnalytics, + code: 'nobidAnalytics', + gvlid: GVLID +}); +nobidAnalytics.originalAdUnits = {}; +window.nobidCarbonizer = { + getStoredLocalData: function () { + const a = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + const b = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + const ret = {}; + if (a) ret[nobidAnalytics.ANALYTICS_DATA_NAME] = a; + if (b) ret[nobidAnalytics.ANALYTICS_OPT_NAME] = b + return ret; + }, + isActive: function () { + let stored = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return false; + return stored.carbonizer_active || false; + }, + carbonizeAdunits: function (adunits, skipTestGroup) { + function processBlockedBidders (blockedBidders) { + function sendOptimizerData() { + let optData = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + storage.removeDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + if (isJson(optData)) { + optData = JSON.parse(optData); + if (Object.getOwnPropertyNames(optData).length > 0) { + const event = { o_bidders: optData }; + if (nobidAnalytics.topLocation) event.topLocation = nobidAnalytics.topLocation; + sendEvent(event, 'optData'); + } + } + } + if (blockedBidders && blockedBidders.length > 0) { + let optData = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + optData = isJson(optData) ? JSON.parse(optData) : {}; + const bidders = blockedBidders.map(rec => rec.bidder); + if (bidders && bidders.length > 0) { + bidders.forEach(bidder => { + if (!optData[bidder]) optData[bidder] = 1; + else optData[bidder] += 1; + }); + storage.setDataInLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME, JSON.stringify(optData)); + if (window.nobidAnalyticsOptTimer) return; + window.nobidAnalyticsOptTimer = setInterval(sendOptimizerData, ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS); + } + } + } + function carbonizeAdunit (adunit) { + let stored = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return; + const carbonizerBidders = stored.bidders || []; + let originalAdUnit = null; + if (nobidAnalytics.originalAdUnits && nobidAnalytics.originalAdUnits[adunit.code]) originalAdUnit = nobidAnalytics.originalAdUnits[adunit.code]; + const allowedBidders = originalAdUnit.bids.filter(rec => carbonizerBidders.includes(rec.bidder)); + const blockedBidders = originalAdUnit.bids.filter(rec => !carbonizerBidders.includes(rec.bidder)); + processBlockedBidders(blockedBidders); + adunit.bids = allowedBidders; + } + for (const adunit of adunits) { + if (!nobidAnalytics.originalAdUnits[adunit.code]) nobidAnalytics.originalAdUnits[adunit.code] = JSON.parse(JSON.stringify(adunit)); + }; + if (this.isActive()) { + // 5% of the time do not block; + if (!skipTestGroup && Math.floor(Math.random() * 101) <= TEST_ALLOCATION_PERCENTAGE) return; + for (const adunit of adunits) { + carbonizeAdunit(adunit); + }; + } + } +}; +export default nobidAnalytics; diff --git a/modules/nobidAnalyticsAdapter.md b/modules/nobidAnalyticsAdapter.md new file mode 100644 index 00000000000..92b9bdbb3cb --- /dev/null +++ b/modules/nobidAnalyticsAdapter.md @@ -0,0 +1,38 @@ +# Overview +Module Name: NoBid Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [nobid.io](https://nobid.io) + + +# NoBid Analytics Registration + +The NoBid Analytics Adapter is free to use during our Beta period, but requires a simple registration with NoBid. Please visit [www.nobid.io](https://www.nobid.io/contact-1/) to sign up and request your NoBid Site ID to get started. If you're already using the NoBid Prebid Adapter, you may use your existing Site ID with the NoBid Analytics Adapter. + +The NoBid privacy policy is at [nobid.io/privacy-policy](https://www.nobid.io/privacy-policy/). + +## NoBid Analytics Configuration + +First, make sure to add the NoBid Analytics submodule to your Prebid.js package with: + +``` +gulp build --modules=...,nobidAnalyticsAdapter... +``` + +The following configuration parameters are available: + +```javascript +pbjs.enableAnalytics({ + provider: 'nobidAnalytics', + options: { + siteId: 123 // change to the Site ID you received from NoBid + } +}); +``` + +{: .table .table-bordered .table-striped } +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| provider | Required | String | The name of this module: `nobidAnalytics` | `nobidAnalytics` | +| options.siteId | Required | Number | This is the NoBid Site ID Number obtained from registering with NoBid. | `1234` | diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 7bf1c4b80db..28fb38e14e5 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -3,7 +3,16 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const GVLID = 816; const BIDDER_CODE = 'nobid'; @@ -63,6 +72,19 @@ function nobidBuildRequests(bids, bidderRequest) { } return uspConsent; } + var gppConsent = function(bidderRequest) { + let gppConsent = null; + if (bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent?.applicableSections) { + gppConsent = {}; + gppConsent.gpp = bidderRequest.gppConsent.gppString; + gppConsent.gpp_sid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections : []; + } else if (bidderRequest?.ortb2?.regs?.gpp && bidderRequest?.ortb2.regs?.gpp_sid) { + gppConsent = {}; + gppConsent.gpp = bidderRequest.ortb2.regs.gpp; + gppConsent.gpp_sid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid : []; + } + return gppConsent; + } var schain = function(bids) { if (bids && bids.length > 0) { return bids[0].schain @@ -145,6 +167,9 @@ function nobidBuildRequests(bids, bidderRequest) { if (cop) state['coppa'] = cop; const eids = getEIDs(deepAccess(bids, '0.userIdAsEids')); if (eids && eids.length > 0) state['eids'] = eids; + const gpp = gppConsent(bidderRequest); + if (gpp?.gpp) state['gpp'] = gpp.gpp; + if (gpp?.gpp_sid) state['gpp_sid'] = gpp.gpp_sid; if (bidderRequest && bidderRequest.ortb2) state['ortb2'] = bidderRequest.ortb2; return state; }; @@ -230,9 +255,9 @@ function nobidBuildRequests(bids, bidderRequest) { siteId = (typeof bid.params['siteId'] != 'undefined' && bid.params['siteId']) ? bid.params['siteId'] : siteId; var placementId = bid.params['placementId']; - var adType = 'banner'; + let adType = 'banner'; const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); + const context = deepAccess(bid, 'mediaTypes.video.context') || ''; if (bid.mediaType === VIDEO || (videoMediaType && (context === 'instream' || context === 'outstream'))) { adType = 'video'; } @@ -246,7 +271,8 @@ function nobidBuildRequests(bids, bidderRequest) { placementId: placementId, ad_type: adType, params: bid.params, - floor: floor + floor: floor, + ctx: context }, adunits); } @@ -350,25 +376,26 @@ export const spec = { ], supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * 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) { log('isBidRequestValid', bid); return !!bid.params.siteId; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { function resolveEndpoint() { var ret = 'https://ads.servenobid.com/'; var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; if (!env) ret = 'https://ads.servenobid.com/'; else if (env == 'beta') ret = 'https://beta.servenobid.com/'; else if (env == 'dev') ret = '//localhost:8282/'; @@ -403,11 +430,11 @@ export const spec = { }; }, /** - * 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. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { log('interpretResponse -> serverResponse', serverResponse); log('interpretResponse -> bidRequest', bidRequest); @@ -415,13 +442,13 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, usPrivacy) { + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, usPrivacy, gppConsent) { if (syncOptions.iframeEnabled) { let params = ''; if (gdprConsent && typeof gdprConsent.consentString === 'string') { @@ -437,6 +464,12 @@ export const spec = { else params += '?'; params += 'usp_consent=' + usPrivacy; } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + if (params.length > 0) params += '&'; + else params += '?'; + params += 'gpp=' + encodeURIComponent(gppConsent.gppString); + params += 'gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(',')); + } return [{ type: 'iframe', url: 'https://public.servenobid.com/sync.html' + params @@ -459,9 +492,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if bidder timed out after an auction - * @param {data} Containing timeout specific data - */ + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ onTimeout: function(data) { window.nobid.timeoutTotal++; log('Timeout total: ' + window.nobid.timeoutTotal, data); diff --git a/modules/nobidBidAdapter.md b/modules/nobidBidAdapter.md index 4449ad5c88b..e2f1c75e782 100644 --- a/modules/nobidBidAdapter.md +++ b/modules/nobidBidAdapter.md @@ -7,6 +7,7 @@ hide: true media_types: banner, video gdpr_supported: true usp_supported: true +gpp_supported: true --- ### Bid Params diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index 7eced81d35e..b6eab776df2 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -11,15 +11,20 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'novatiq'; /** @type {Submodule} */ export const novatiqIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** * used to specify vendor id @@ -28,10 +33,10 @@ export const novatiqIdSubmodule = { gvlid: 1119, /** - * decode the stored id value for passing to bid requests - * @function - * @returns {novatiq: {snowflake: string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @returns {novatiq: {snowflake: string}} + */ decode(novatiqId, config) { let responseObj = { novatiq: { @@ -52,11 +57,11 @@ export const novatiqIdSubmodule = { }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} config - * @returns {id: string} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @returns {id: string} + */ getId(config) { const configParams = config.params || {}; const urlParams = this.getUrlParams(configParams); diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 4fd9b711b42..9937391f6e7 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -1,9 +1,10 @@ 'use strict'; import {BANNER} from '../src/mediaTypes.js'; -import {getAdUnitSizes, getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; +import {getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ajax} from '../src/ajax.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ogury'; const GVLID = 31; @@ -11,7 +12,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.5.0'; +const ADAPTER_VERSION = '1.6.0'; function getClientWidth() { const documentElementClientWidth = window.top.document.documentElement.clientWidth @@ -121,6 +122,13 @@ function buildRequests(validBidRequests, bidderRequest) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; const floor = getFloor(bidRequest); + if (bidRequest.userId) { + openRtbBidRequestBanner.user.ext.uids = bidRequest.userId + } + if (bidRequest.userIdAsEids) { + openRtbBidRequestBanner.user.ext.eids = bidRequest.userIdAsEids + } + openRtbBidRequestBanner.imp.push({ id: bidRequest.bidId, tagid: bidRequest.params.adUnitId, diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js new file mode 100644 index 00000000000..e6c8f8b098e --- /dev/null +++ b/modules/omsBidAdapter.js @@ -0,0 +1,283 @@ +import { + isArray, + getWindowTop, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, + getBidIdParameter, + getUniqueIdentifierStr, +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {ajax} from '../src/ajax.js'; +import {percentInView} from '../libraries/percentInView/percentInView.js'; + +const BIDDER_CODE = 'oms'; +const URL = 'https://rt.marphezis.com/hb'; +const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid' + +export const spec = { + code: BIDDER_CODE, + aliases: ['brightcom', 'bcmssp'], + gvlid: 883, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidderError, + onBidWon, + getUserSyncs, +}; + +function buildRequests(bidReqs, bidderRequest) { + try { + const impressions = bidReqs.map(bid => { + let bidSizes = bid?.mediaTypes?.banner?.sizes || bid.sizes; + bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); + bidSizes = bidSizes.filter(size => isArray(size)); + const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); + + const element = document.getElementById(bid.adUnitCode); + const minSize = _getMinSize(processedSizes); + const viewabilityAmount = _isViewabilityMeasurable(element) ? _getViewability(element, getWindowTop(), minSize) : 'na'; + const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); + const gpidData = _extractGpidData(bid); + + const imp = { + id: bid.bidId, + banner: { + format: processedSizes, + ext: { + viewability: viewabilityAmountRounded, + } + }, + ext: { + ...gpidData + }, + tagid: String(bid.adUnitCode) + }; + + const bidFloor = _getBidFloor(bid); + + if (bidFloor) { + imp.bidfloor = bidFloor; + } + + return imp; + }) + + const referrer = bidderRequest?.refererInfo?.page || ''; + const publisherId = getBidIdParameter('publisherId', bidReqs[0].params); + + const payload = { + id: getUniqueIdentifierStr(), + imp: impressions, + site: { + domain: bidderRequest?.refererInfo?.domain || '', + page: referrer, + publisher: { + id: publisherId + } + }, + device: { + devicetype: _getDeviceType(navigator.userAgent, bidderRequest?.ortb2?.device?.sua), + w: screen.width, + h: screen.height + }, + tmax: bidderRequest?.timeout + }; + + if (bidderRequest?.gdprConsent) { + deepSetValue(payload, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); + deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + const gpp = _getGpp(bidderRequest) + if (gpp) { + deepSetValue(payload, 'regs.ext.gpp', gpp); + } + + if (bidderRequest?.ortb2?.regs?.coppa) { + deepSetValue(payload, 'regs.coppa', 1); + } + + if (bidReqs?.[0]?.schain) { + deepSetValue(payload, 'source.ext.schain', bidReqs[0].schain) + } + + if (bidderRequest?.ortb2?.user) { + deepSetValue(payload, 'user', bidderRequest.ortb2.user) + } + + if (bidReqs?.[0]?.userIdAsEids) { + deepSetValue(payload, 'user.ext.eids', bidReqs[0].userIdAsEids || []) + } + + if (bidReqs?.[0].userId) { + deepSetValue(payload, 'user.ext.ids', bidReqs[0].userId || []) + } + + if (bidderRequest?.ortb2?.site?.content) { + deepSetValue(payload, 'site.content', bidderRequest.ortb2.site.content) + } + + return { + method: 'POST', + url: URL, + data: JSON.stringify(payload), + }; + } catch (e) { + logError(e, {bidReqs, bidderRequest}); + } +} + +function isBidRequestValid(bid) { + if (bid.bidder !== BIDDER_CODE || !bid.params || !bid.params.publisherId) { + return false; + } + + return true; +} + +function interpretResponse(serverResponse) { + let response = []; + if (!serverResponse.body || typeof serverResponse.body != 'object') { + logWarn('OMS server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); + return response; + } + + const {body: {id, seatbid}} = serverResponse; + + try { + if (id && seatbid && seatbid.length > 0 && seatbid[0].bid && seatbid[0].bid.length > 0) { + response = seatbid[0].bid.map(bid => { + return { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: _getAdMarkup(bid), + ttl: 300, + meta: { + advertiserDomains: bid?.adomain || [] + } + }; + }); + } + } catch (e) { + logError(e, {id, seatbid}); + } + + return response; +} + +// Don't do user sync for now +function getUserSyncs(syncOptions, responses, gdprConsent) { + return []; +} + +function onBidderError(errorData) { + if (errorData === null || !errorData.bidderRequest) { + return; + } + + _trackEvent('error', errorData.bidderRequest) +} + +function onBidWon(bid) { + if (bid === null) { + return; + } + + _trackEvent('bidwon', bid) +} + +function _trackEvent(endpoint, data) { + ajax(`${TRACK_EVENT_URL}/${endpoint}`, null, JSON.stringify(data), { + method: 'POST', + withCredentials: false + }); +} + +function _getDeviceType(ua, sua) { + if (sua?.mobile || (/(ios|ipod|ipad|iphone|android)/i).test(ua)) { + return 1 + } + + if ((/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(ua)) { + return 3 + } + + return 2 +} + +function _getGpp(bidderRequest) { + if (bidderRequest?.gppConsent != null) { + return bidderRequest.gppConsent; + } + + return ( + bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' } + ); +} + +function _getAdMarkup(bid) { + let adm = bid.adm; + if ('nurl' in bid) { + adm += createTrackPixelHtml(bid.nurl); + } + return adm; +} + +function _isViewabilityMeasurable(element) { + return !_isIframe() && element !== null; +} + +function _getViewability(element, topWin, {w, h} = {}) { + return getWindowTop().document.visibilityState === 'visible' ? percentInView(element, topWin, {w, h}) : 0; +} + +function _extractGpidData(bid) { + return { + gpid: bid?.ortb2Imp?.ext?.gpid, + adserverName: bid?.ortb2Imp?.ext?.data?.adserver?.name, + adslot: bid?.ortb2Imp?.ext?.data?.adserver?.adslot, + pbadslot: bid?.ortb2Imp?.ext?.data?.pbadslot, + } +} + +function _isIframe() { + try { + return getWindowSelf() !== getWindowTop(); + } catch (e) { + return true; + } +} + +function _getMinSize(sizes) { + return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); +} + +function _getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return bid.params.bidFloor ? bid.params.bidFloor : null; + } + + let floor = bid.getFloor({ + currency: 'USD', mediaType: '*', size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/omsBidAdapter.md b/modules/omsBidAdapter.md new file mode 100644 index 00000000000..f1e2d459eca --- /dev/null +++ b/modules/omsBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: OMS Bid Adapter +Module Type: Bidder Adapter +Maintainer: alexandruc@onlinemediasolutions.com +``` + +# Description + +Online media solutions adapter integration to the Prebid library. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'test-leaderboard', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [{ + bidder: 'oms', + params: { + publisherId: 2141020, + bidFloor: 0.01 + } + }] + }, { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'oms', + params: { + publisherId: 2141020 + } + }] + } +] +``` diff --git a/modules/oneKeyIdSystem.js b/modules/oneKeyIdSystem.js index 699a7a6ab95..8765a72a1af 100644 --- a/modules/oneKeyIdSystem.js +++ b/modules/oneKeyIdSystem.js @@ -8,6 +8,13 @@ import {submodule} from '../src/hook.js'; import { logError, logMessage } from '../src/utils.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + // Pre-init OneKey if it has not load yet. window.OneKey = window.OneKey || {}; window.OneKey.queue = window.OneKey.queue || []; @@ -47,27 +54,27 @@ const getIdsAndPreferences = (callback) => { /** @type {Submodule} */ export const oneKeyIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: 'oneKeyData', /** - * decode the stored data value for passing to bid requests - * @function decode - * @param {(Object|string)} value - * @returns {(Object|undefined)} - */ + * decode the stored data value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ decode(data) { return { oneKeyData: data }; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @param {(Object|undefined)} cacheIdObj - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ getId(config) { return { callback: getIdsAndPreferences diff --git a/modules/oneKeyRtdProvider.js b/modules/oneKeyRtdProvider.js index 27511017676..19915609820 100644 --- a/modules/oneKeyRtdProvider.js +++ b/modules/oneKeyRtdProvider.js @@ -3,6 +3,10 @@ import { submodule } from '../src/hook.js'; import { mergeDeep, logError, logMessage, deepSetValue, generateUUID } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const SUBMODULE_NAME = 'oneKey'; const prefixLog = 'OneKey.RTD-module' diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 724a53a3095..d8423253aaf 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -8,6 +8,11 @@ import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { deepClone, logError, deepAccess } from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; @@ -58,7 +63,8 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.gdprConsent) { payload.gdprConsent = { consentString: bidderRequest.gdprConsent.consentString, - consentRequired: bidderRequest.gdprConsent.gdprApplies + consentRequired: bidderRequest.gdprConsent.gdprApplies, + addtlConsent: bidderRequest.gdprConsent.addtlConsent }; } if (bidderRequest && bidderRequest.gppConsent) { @@ -70,6 +76,9 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.ortb2) { + payload.ortb2 = bidderRequest.ortb2; + } if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userIdAsEids) { payload.userId = validBidRequests[0].userIdAsEids; } @@ -84,6 +93,7 @@ function buildRequests(validBidRequests, bidderRequest) { const connection = navigator.connection || navigator.webkitConnection; payload.networkConnectionType = (connection && connection.type) ? connection.type : null; payload.networkEffectiveConnectionType = (connection && connection.effectiveType) ? connection.effectiveType : null; + payload.fledgeEnabled = Boolean(bidderRequest && bidderRequest.fledgeEnabled) return { method: 'POST', url: ENDPOINT, @@ -98,10 +108,10 @@ function interpretResponse(serverResponse, bidderRequest) { if (!body || (body.nobid && body.nobid === true)) { return bids; } - if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { + if (!body.fledgeAuctionConfigs && (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0)) { return bids; } - body.bids.forEach(bid => { + Array.isArray(body.bids) && body.bids.forEach(bid => { const responseBid = { requestId: bid.requestId, cpm: bid.cpm, @@ -118,6 +128,9 @@ function interpretResponse(serverResponse, bidderRequest) { }, ttl: bid.ttl || 300 }; + if (bid.dsa) { + responseBid.meta.dsa = bid.dsa; + } if (bid.mediaType === BANNER) { responseBid.ad = bid.ad; } else if (bid.mediaType === VIDEO) { @@ -138,7 +151,16 @@ function interpretResponse(serverResponse, bidderRequest) { } bids.push(responseBid); }); - return bids; + + if (body.fledgeAuctionConfigs && Array.isArray(body.fledgeAuctionConfigs)) { + const fledgeAuctionConfigs = body.fledgeAuctionConfigs + return { + bids, + fledgeAuctionConfigs, + } + } else { + return bids; + } } function createRenderer(bid, rendererOptions = {}) { @@ -264,9 +286,8 @@ function setGeneralInfo(bidRequest) { this['adUnitCode'] = bidRequest.adUnitCode; this['bidId'] = bidRequest.bidId; this['bidderRequestId'] = bidRequest.bidderRequestId; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - this['auctionId'] = bidRequest.auctionId; - this['transactionId'] = bidRequest.ortb2Imp?.ext?.tid; + this['auctionId'] = deepAccess(bidRequest, 'ortb2.source.tid'); + this['transactionId'] = deepAccess(bidRequest, 'ortb2Imp.ext.tid'); this['gpid'] = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); this['pubId'] = params.pubId; this['ext'] = params.ext; diff --git a/modules/onomagicBidAdapter.js b/modules/onomagicBidAdapter.js index edab625e541..78f00153a8b 100644 --- a/modules/onomagicBidAdapter.js +++ b/modules/onomagicBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - createTrackPixelHtml, - getBidIdParameter, + createTrackPixelHtml, getBidIdParameter, getUniqueIdentifierStr, getWindowSelf, getWindowTop, diff --git a/modules/open8BidAdapter.js b/modules/open8BidAdapter.js index 5fa1dd0a143..49523926c0e 100644 --- a/modules/open8BidAdapter.js +++ b/modules/open8BidAdapter.js @@ -1,8 +1,9 @@ import { Renderer } from '../src/Renderer.js'; import {ajax} from '../src/ajax.js'; -import { createTrackPixelHtml, getBidIdParameter, logError, logWarn, tryAppendQueryString } from '../src/utils.js'; +import {createTrackPixelHtml, getBidIdParameter, logError, logWarn} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'open8'; const URL = 'https://as.vt.open8.com/v1/control/prebid'; diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 296bfc682f1..39bd50f61a9 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,248 +1,487 @@ -import {convertTypes, deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + triggerPixel, + isInteger, + getBidIdParameter, + getDNT +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -import {find} from '../src/polyfill.js'; -const ENDPOINT = 'https://ghb.spotim.market/v2/auction'; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'openweb'; -const DISPLAY = 'display'; -const syncsCache = {}; +const ADAPTER_VERSION = '6.0.0'; +const TTL = 360; +const DEFAULT_CURRENCY = 'USD'; +const SELLER_ENDPOINT = 'https://hb.openwebmp.com/'; +const MODES = { + PRODUCTION: 'hb-multi', + TEST: 'hb-multi-test' +} +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} export const spec = { code: BIDDER_CODE, gvlid: 280, - supportedMediaTypes: [VIDEO, BANNER, ADPOD], - isBidRequestValid: function (bid) { - return isNumber(deepAccess(bid, 'params.aid')); - }, - getUserSyncs: function (syncOptions, serverResponses) { - const syncs = []; + version: ADAPTER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function (bidRequest) { + if (!bidRequest.params) { + logWarn('no params have been set to OpenWeb adapter'); + return false; + } - function addSyncs(bid) { - const uris = bid.cookieURLs; - const types = bid.cookieURLSTypes || []; + if (!bidRequest.params.org) { + logWarn('org is a mandatory param for OpenWeb adapter'); + return false; + } - if (Array.isArray(uris)) { - uris.forEach((uri, i) => { - const type = types[i] || 'image'; + return true; + }, + buildRequests: function (validBidRequests, bidderRequest) { + const combinedRequestsObject = {}; - if ((!syncOptions.pixelEnabled && type === 'image') || - (!syncOptions.iframeEnabled && type === 'iframe') || - syncsCache[uri]) { - return; - } + // use data from the first bid, to create the general params for all bids + const generalObject = validBidRequests[0]; + const testMode = generalObject.params.testMode; - syncsCache[uri] = true; - syncs.push({ - type: type, - url: uri - }) - }) - } + combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); + combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); + + return { + method: 'POST', + url: getEndpoint(testMode), + data: combinedRequestsObject } + }, + interpretResponse: function ({body}) { + const bidResponses = []; - if (syncOptions.pixelEnabled || syncOptions.iframeEnabled) { - isArray(serverResponses) && serverResponses.forEach((response) => { - if (response.body) { - if (isArray(response.body)) { - response.body.forEach(b => { - addSyncs(b); - }) - } else { - addSyncs(response.body) + if (body && body.bids && body.bids.length) { + body.bids.forEach(adUnit => { + const bidResponse = { + requestId: adUnit.requestId, + cpm: adUnit.cpm, + currency: adUnit.currency || DEFAULT_CURRENCY, + width: adUnit.width, + height: adUnit.height, + ttl: adUnit.ttl || TTL, + creativeId: adUnit.requestId, + netRevenue: adUnit.netRevenue || true, + nurl: adUnit.nurl, + mediaType: adUnit.mediaType, + meta: { + mediaType: adUnit.mediaType } + }; + + if (adUnit.mediaType === VIDEO) { + bidResponse.vastXml = adUnit.vastXml; + } else if (adUnit.mediaType === BANNER) { + bidResponse.ad = adUnit.ad; } - }) - } - return syncs; - }, - /** - * Make a server request from the list of BidRequests - * @param bidRequests - * @param adapterRequest - */ - buildRequests: function (bidRequests, adapterRequest) { - const { tag, bids } = bidToTag(bidRequests, adapterRequest); - return [{ - data: Object.assign({}, tag, { BidRequests: bids }), - adapterRequest, - method: 'POST', - url: ENDPOINT - }]; - }, - /** - * Unpack the response from the server into a list of bids - * @param serverResponse - * @param bidderRequest - * @return {Bid[]} An array of bids which were nested inside the server - */ - interpretResponse: function (serverResponse, { adapterRequest }) { - serverResponse = serverResponse.body; - let bids = []; + if (adUnit.adomain && adUnit.adomain.length) { + bidResponse.meta.advertiserDomains = adUnit.adomain; + } - if (!isArray(serverResponse)) { - return parseRTBResponse(serverResponse, adapterRequest); + bidResponses.push(bidResponse); + }); } - serverResponse.forEach(serverBidResponse => { - bids = flatten(bids, parseRTBResponse(serverBidResponse, adapterRequest)); - }); - - return bids; + return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.params.userSyncURL + }); + } + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } - transformBidParams(params) { - return convertTypes({ - 'aid': 'number', - }, params); + logInfo('onBidWon:', bid); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } } }; -function parseRTBResponse(serverResponse, adapterRequest) { - const isEmptyResponse = !serverResponse || !isArray(serverResponse.bids); - const bids = []; +registerBidder(spec); - if (isEmptyResponse) { - return bids; +/** + * Get floor price + * @param bid {bid} + * @returns {Number} + */ +function getFloor(bid, mediaType, currency) { + if (!isFn(bid.getFloor)) { + return 0; } + let floorResult = bid.getFloor({ + currency: currency, + mediaType: mediaType, + size: '*' + }); + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; +} - serverResponse.bids.forEach(serverBid => { - const request = find(adapterRequest.bids, (bidRequest) => { - return bidRequest.bidId === serverBid.requestId; - }); +/** + * Get the the ad sizes array from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizesArray(bid, mediaType) { + let sizesArray = [] - if (serverBid.cpm !== 0 && request !== undefined) { - const bid = createBid(serverBid, request); + if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { + sizesArray = bid.mediaTypes[mediaType].sizes; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizesArray = bid.sizes; + } - bids.push(bid); - } + return sizesArray; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; }); + return scStr; +} - return bids; +/** + * Get encoded node value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !isEmpty(val) ? encodeURIComponent(val) : ''; } -function bidToTag(bidRequests, adapterRequest) { - // start publisher env - const tag = { - // TODO: is 'page' the right value here? - Domain: deepAccess(adapterRequest, 'refererInfo.page') - }; - if (config.getConfig('coppa') === true) { - tag.Coppa = 1; +/** + * Get preferred user-sync method based on publisher configuration + * @param bidderCode {string} + * @returns {string} + */ +function getAllowedSyncMethod(filterSettings, bidderCode) { + const iframeConfigsToCheck = ['all', 'iframe']; + const pixelConfigToCheck = 'image'; + if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; } - if (deepAccess(adapterRequest, 'gdprConsent.gdprApplies')) { - tag.GDPR = 1; - tag.GDPRConsent = deepAccess(adapterRequest, 'gdprConsent.consentString'); + if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; } - if (deepAccess(adapterRequest, 'uspConsent')) { - tag.USP = deepAccess(adapterRequest, 'uspConsent'); +} + +/** + * Check if sync rule is supported + * @param syncRule {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(syncRule, bidderCode) { + if (!syncRule) { + return false; } - if (deepAccess(bidRequests[0], 'schain')) { - tag.Schain = deepAccess(bidRequests[0], 'schain'); + const isInclude = syncRule.filter === 'include'; + const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && contains(bidders, bidderCode); +} + +/** + * Get the seller endpoint + * @param testMode {boolean} + * @returns {string} + */ +function getEndpoint(testMode) { + return testMode + ? SELLER_ENDPOINT + MODES.TEST + : SELLER_ENDPOINT + MODES.PRODUCTION; +} + +/** + * get device type + * @param uad {ua} + * @returns {string} + */ +function getDeviceType(ua) { + if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i + .test(ua.toLowerCase())) { + return '5'; } - if (deepAccess(bidRequests[0], 'userId')) { - tag.UserIds = deepAccess(bidRequests[0], 'userId'); + if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i + .test(ua.toLowerCase())) { + return '4'; } - if (deepAccess(bidRequests[0], 'userIdAsEids')) { - tag.UserEids = deepAccess(bidRequests[0], 'userIdAsEids'); + if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i + .test(ua.toLowerCase())) { + return '3'; } - // end publisher env - const bids = []; + return '1'; +} + +function generateBidsParams(validBidRequests, bidderRequest) { + const bidsArray = []; - for (let i = 0, length = bidRequests.length; i < length; i++) { - const bid = prepareBidRequests(bidRequests[i]); - bids.push(bid); + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); } - return { tag, bids }; + return bidsArray; } /** - * Parse mediaType - * @param bidReq {object} - * @returns {object} + * Generate bid specific parameters + * @param {bid} bid + * @param {bidderRequest} bidderRequest + * @returns {Object} bid specific params object */ -function prepareBidRequests(bidReq) { - const mediaType = deepAccess(bidReq, 'mediaTypes.video') ? VIDEO : DISPLAY; - const sizes = mediaType === VIDEO ? deepAccess(bidReq, 'mediaTypes.video.playerSize') : deepAccess(bidReq, 'mediaTypes.banner.sizes'); - const bidReqParams = { - 'CallbackId': bidReq.bidId, - 'Aid': bidReq.params.aid, - 'AdType': mediaType, - 'Sizes': parseSizesInput(sizes).join(',') +function generateBidParameters(bid, bidderRequest) { + const {params} = bid; + const mediaType = isBanner(bid) ? BANNER : VIDEO; + const sizesArray = getSizesArray(bid, mediaType); + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; + + // fix floor price in case of NAN + if (isNaN(params.floorPrice)) { + params.floorPrice = 0; + } + + const bidObject = { + mediaType, + adUnitCode: getBidIdParameter('adUnitCode', bid), + sizes: sizesArray, + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), + bidId: getBidIdParameter('bidId', bid), + loop: getBidIdParameter('bidderRequestsCount', bid), + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + transactionId: bid.ortb2Imp?.ext?.tid || '', + coppa: 0, }; - bidReqParams.PlacementId = bidReq.adUnitCode; - if (bidReq.params.iframe) { - bidReqParams.AdmType = 'iframe'; + const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); + if (pos) { + bidObject.pos = pos; + } + + const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); + if (gpid) { + bidObject.gpid = gpid; } + + const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); + if (placementId) { + bidObject.placementId = placementId; + } + + const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); + if (mimes) { + bidObject.mimes = mimes; + } + const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); + if (api) { + bidObject.api = api; + } + + const sua = deepAccess(bid, `ortb2.device.sua`); + if (sua) { + bidObject.sua = sua; + } + + const coppa = deepAccess(bid, `ortb2.regs.coppa`); + if (coppa) { + bidObject.coppa = 1; + } + if (mediaType === VIDEO) { - const context = deepAccess(bidReq, 'mediaTypes.video.context'); - if (context === ADPOD) { - bidReqParams.Adpod = deepAccess(bidReq, 'mediaTypes.video'); + const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); + let playbackMethodValue; + + // verify playbackMethod is of type integer array, or integer only. + if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { + // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation + playbackMethodValue = playbackMethod[0]; + } else if (isInteger(playbackMethod)) { + playbackMethodValue = playbackMethod; + } + + if (playbackMethodValue) { + bidObject.playbackMethod = playbackMethodValue; + } + + const placement = deepAccess(bid, `mediaTypes.video.placement`); + if (placement) { + bidObject.placement = placement; + } + + const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); + if (minDuration) { + bidObject.minDuration = minDuration; + } + + const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); + if (maxDuration) { + bidObject.maxDuration = maxDuration; + } + + const skip = deepAccess(bid, `mediaTypes.video.skip`); + if (skip) { + bidObject.skip = skip; + } + + const linearity = deepAccess(bid, `mediaTypes.video.linearity`); + if (linearity) { + bidObject.linearity = linearity; + } + + const protocols = deepAccess(bid, `mediaTypes.video.protocols`); + if (protocols) { + bidObject.protocols = protocols; + } + + const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); + if (plcmt) { + bidObject.plcmt = plcmt; } } - return bidReqParams; + + return bidObject; } -/** - * Prepare all parameters for request - * @param bidderRequest {object} - * @returns {object} - */ -function getMediaType(bidderRequest) { - return deepAccess(bidderRequest, 'mediaTypes.video') ? VIDEO : BANNER; +function isBanner(bid) { + return bid.mediaTypes && bid.mediaTypes.banner; } /** - * Configure new bid by response - * @param bidResponse {object} - * @param bidRequest {Object} - * @returns {object} + * Generate params that are common between all bids + * @param {single bid object} generalObject + * @param {bidderRequest} bidderRequest + * @returns {object} the common params object */ -function createBid(bidResponse, bidRequest) { - const mediaType = getMediaType(bidRequest) - const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - const bid = { - requestId: bidResponse.requestId, - creativeId: bidResponse.cmpId, - height: bidResponse.height, - currency: bidResponse.cur, - width: bidResponse.width, - cpm: bidResponse.cpm, - netRevenue: true, - mediaType, - ttl: 300, - meta: { - advertiserDomains: bidResponse.adomain || [] +function generateGeneralParams(generalObject, bidderRequest) { + const domain = deepAccess(bidderRequest, 'refererInfo.domain') || window.location.hostname; + const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; + const {bidderCode, timeout} = bidderRequest; + const generalBidParams = generalObject.params; + + // these params are snake_case instead of camelCase to allow backwards compatability on the server. + // in the future, these will be converted to camelCase to match our convention. + const generalParams = { + wrapper_type: 'prebidjs', + wrapper_vendor: '$$PREBID_GLOBAL$$', + wrapper_version: '$prebid.version$', + adapter_version: ADAPTER_VERSION, + publisher_id: generalBidParams.org, + publisher_name: domain, + site_domain: domain, + dnt: getDNT() ? 1 : 0, + device_type: getDeviceType(navigator.userAgent), + ua: navigator.userAgent, + is_wrapper: !!generalBidParams.isWrapper, + session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), + tmax: timeout + } + + const userIdsParam = getBidIdParameter('userId', generalObject); + if (userIdsParam) { + generalParams.userIds = JSON.stringify(userIdsParam); + } + + const ortb2Metadata = bidderRequest.ortb2 || {}; + if (ortb2Metadata.site) { + generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); + } + if (ortb2Metadata.user) { + generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + generalParams.cs_method = allowedSyncMethod; } - }; + } - if (mediaType === BANNER) { - return Object.assign(bid, { - ad: bidResponse.ad, - adUrl: bidResponse.adUrl, - }); + if (bidderRequest.auctionStart) { + generalParams.auction_start = bidderRequest.auctionStart; } - if (context === ADPOD) { - Object.assign(bid, { - meta: { - primaryCatId: bidResponse.primaryCatId, - }, - video: { - context: ADPOD, - durationSeconds: bidResponse.durationSeconds - } - }); + + if (bidderRequest.uspConsent) { + generalParams.us_privacy = bidderRequest.uspConsent; } - Object.assign(bid, { - vastUrl: bidResponse.vastUrl - }); + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } - return bid; -} + if (bidderRequest.gppConsent) { + generalParams.gpp = bidderRequest.gppConsent.gppString; + generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + generalParams.gpp = bidderRequest.ortb2.regs.gpp; + generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } -registerBidder(spec); + if (generalBidParams.ifa) { + generalParams.ifa = generalBidParams.ifa; + } + + if (generalObject.schain) { + generalParams.schain = getSupplyChain(generalObject.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); + generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); + } + + return generalParams; +} diff --git a/modules/openwebBidAdapter.md b/modules/openwebBidAdapter.md index dc8bfa6c59e..36c1f0ca6c5 100644 --- a/modules/openwebBidAdapter.md +++ b/modules/openwebBidAdapter.md @@ -1,27 +1,53 @@ # Overview -**Module Name**: OpenWeb Bidder Adapter -**Module Type**: Bidder Adapter -**Maintainer**: monetization@openweb.com +Module Name: OpenWeb Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: monetization@openweb.com + # Description -OpenWeb.com official prebid adapter. Available in both client and server side versions. -OpenWeb header bidding adapter provides solution for accessing both Video and Display demand. +Module that connects to OpenWeb's demand sources. + +The OpenWeb adapter requires setup and approval from OpenWeb. Please reach out to monetization@openweb.com to create an OpenWeb account. + +The adapter supports Video and Display demand. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `org` | required | String | OpenWeb publisher Id provided by your OpenWeb representative | "1234567890abcdef12345678" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 +| `placementId` | optional | String | A unique placement identifier | "12345678" +| `testMode` | optional | Boolean | This activates the test mode | false +| `currency` | optional | String | 3 letters currency | "EUR" + # Test Parameters -``` - var adUnits = [ - // Banner adUnit - { - code: 'div-test-div', - sizes: [[300, 250]], +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, bids: [{ bidder: 'openweb', params: { - aid: 529814 + org: '1234567890abcdef12345678', // Required + floorPrice: 2.00, // Optional + placementId: '12345678', // Optional + testMode: false, // Optional, } }] } - ]; -``` + ]; +``` \ No newline at end of file diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 03423a028b4..a99bd1c5325 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -4,6 +4,7 @@ import * as utils from '../src/utils.js'; import {mergeDeep} from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const bidderConfig = 'hb_pb_ortb'; const bidderVersion = '2.0'; @@ -12,6 +13,7 @@ export const SYNC_URL = 'https://u.openx.net/w/1.0/pd'; export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba'; export const spec = { code: 'openx', + gvlid: 69, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, buildRequests, @@ -48,7 +50,8 @@ const converter = ortbConverter({ mergeDeep(req, { at: 1, ext: { - bc: `${bidderConfig}_${bidderVersion}` + bc: `${bidderConfig}_${bidderVersion}`, + pv: '$prebid.version$' } }) const bid = context.bidRequests[0]; @@ -104,9 +107,11 @@ const converter = ortbConverter({ fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { return { bidId, - config: Object.assign({ - auctionSignals: {}, - }, cfg) + config: mergeDeep(Object.assign({}, cfg), { + auctionSignals: { + ortb2Imp: context.impContext[bidId]?.imp, + }, + }), } }); return { @@ -149,7 +154,7 @@ const converter = ortbConverter({ }); function transformBidParams(params, isOpenRtb) { - return utils.convertTypes({ + return convertTypes({ 'unit': 'string', 'customFloor': 'number' }, params); diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index e721fb85fd7..957192d1bec 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -18,6 +18,13 @@ import {Renderer} from '../src/Renderer.js'; import {OUTSTREAM} from '../src/video.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const BIDDER_CODE = 'operaads'; const ENDPOINT = 'https://s.adx.opera.com/ortb/v2/'; @@ -70,7 +77,7 @@ const NATIVE_DEFAULTS = { export const spec = { code: BIDDER_CODE, - + gvlid: 1135, // short code aliases: ['opera'], @@ -232,13 +239,6 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { test: config.getConfig('debug') ? 1 : 0, imp: createImp(bidRequest), device: getDevice(), - site: { - id: String(deepAccess(bidRequest, 'params.publisherId')), - // TODO: does the fallback make sense here? - domain: bidderRequest?.refererInfo?.domain || window.location.host, - page: bidderRequest?.refererInfo?.page, - ref: bidderRequest?.refererInfo?.ref || '', - }, at: 1, bcat: getBcat(bidRequest), cur: [DEFAULT_CURRENCY], @@ -250,6 +250,7 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { buyeruid: getUserId(bidRequest) } } + fulfillInventoryInfo(payload, bidRequest, bidderRequest); const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { @@ -680,6 +681,11 @@ function mapNativeImage(image, type) { * @returns {String} userId */ function getUserId(bidRequest) { + let operaId = deepAccess(bidRequest, 'userId.operaId'); + if (operaId) { + return operaId; + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); if (sharedId) { return sharedId; @@ -759,6 +765,38 @@ function getDevice() { return device; } +/** + * Fulfill inventory info + * + * @param payload + * @param bidRequest + * @param bidderRequest + */ +function fulfillInventoryInfo(payload, bidRequest, bidderRequest) { + let info = deepAccess(bidRequest, 'params.site'); + // 1.If the inventory info for site specified, use the site object provided in params. + let key = 'site'; + if (!isPlainObject(info)) { + info = deepAccess(bidRequest, 'params.app'); + if (isPlainObject(info)) { + // 2.If the inventory info for app specified, use the app object provided in params. + key = 'app'; + } else { + // 3.Otherwise, we use site by default. + info = {}; + } + } + // Fulfill key parameters. + info.id = String(deepAccess(bidRequest, 'params.publisherId')); + info.domain = info.domain || bidderRequest?.refererInfo?.domain || window.location.host; + if (key === 'site') { + info.ref = info.ref || bidderRequest?.refererInfo?.ref || ''; + info.page = info.page || bidderRequest?.refererInfo?.page; + } + + payload[key] = info; +} + /** * Get browser language * diff --git a/modules/operaadsBidAdapter.md b/modules/operaadsBidAdapter.md index 709c67a04a7..6f13eebd7d5 100644 --- a/modules/operaadsBidAdapter.md +++ b/modules/operaadsBidAdapter.md @@ -14,41 +14,43 @@ Module that connects to OperaAds's demand sources ## Bid Parameters -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` -| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` -| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` -| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------------------|-----------------------------------------|-------------------------------------------------| +| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` | +| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` | +| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` | +| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` | +| `site` | optional | Object | The site information. | `{"name": "my_site", "domain": "www.test.com"}` | +| `app` | optional | Object | The app information. | `{"name": "my_app", "ver": "1.1.0"}` | ### Bid Video Parameters Set these parameters to `bid.mediaTypes.video`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `context` | optional | String | `instream` or `outstream`. | `instream` -| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` -| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` -| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` -| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` -| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` -| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` -| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` -| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` +| Name | Scope | Type | Description | Example | +|------------------|----------|------------------------|------------------------------------------------------------------------------------------|----------------------------| +| `context` | optional | String | `instream` or `outstream`. | `instream` | +| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` | +| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` | +| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` | +| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` | +| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` | +| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` | +| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` | +| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` | ### Bid Native Parameters Set these parameters to `bid.nativeParams` or `bid.mediaTypes.native`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` -| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` -| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` -| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` -| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` -| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` | +| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` | +| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` | +| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` | +| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` | +| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` | ## Example @@ -127,7 +129,9 @@ var adUnits = [{ params: { placementId: 's5340077725248', endpointId: 'ep3425464070464', - publisherId: 'pub3054952966336' + publisherId: 'pub3054952966336', + // You might want to specify some application information here if the bid requests are from an application instead of a browser. + app: { 'name': 'my_app', 'bundle': 'test_bundle', 'store_url': 'www.some-store.com', 'ver': '1.1.0' } } }] }]; @@ -135,18 +139,18 @@ var adUnits = [{ ### User Ids -Opera Ads Bid Adapter uses `sharedId`, `pubcid` or `tdid`, please config at least one. +Opera Ads Bid Adapter uses `operaId`, please refer to [`Opera ID System`](./operaadsIdSystem.md). ```javascript pbjs.setConfig({ ..., userSync: { userIds: [{ - name: 'sharedId', + name: 'operaId', storage: { - name: '_sharedID', // name of the 1st party cookie - type: 'cookie', - expires: 30 + name: 'operaId', + type: 'html5', + expires: 14 } }] } diff --git a/modules/operaadsIdSystem.js b/modules/operaadsIdSystem.js new file mode 100644 index 00000000000..7cf5e2ce5e1 --- /dev/null +++ b/modules/operaadsIdSystem.js @@ -0,0 +1,111 @@ +/** + * This module adds operaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/operaadsIdSystem + * @requires module:modules/userId + */ +import * as ajax from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { logMessage, logError } from '../src/utils.js'; + +/** + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const MODULE_NAME = 'operaId'; +const ID_KEY = MODULE_NAME; +const version = '1.0'; +const SYNC_URL = 'https://t.adx.opera.com/identity/'; +const AJAX_TIMEOUT = 300; +const AJAX_OPTIONS = {method: 'GET', withCredentials: true, contentType: 'application/json'}; + +function constructUrl(pairs) { + const queries = []; + for (let key in pairs) { + queries.push(`${key}=${encodeURIComponent(pairs[key])}`); + } + return `${SYNC_URL}?${queries.join('&')}`; +} + +function asyncRequest(url, cb) { + ajax.ajaxBuilder(AJAX_TIMEOUT)( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { uid: operaId } = jsonResponse; + cb(operaId); + return; + } catch (e) { + logError(`${MODULE_NAME}: invalid response`, response); + } + cb(); + }, + error: (err) => { + logError(`${MODULE_NAME}: ID error response`, err); + cb(); + } + }, + null, + AJAX_OPTIONS + ); +} + +export const operaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * @type {string} + */ + version, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} id + * @returns {{'operaId': string}} + */ + decode: (id) => + id != null && id.length > 0 + ? { [ID_KEY]: id } + : undefined, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + logMessage(`${MODULE_NAME}: start synchronizing opera uid`); + const params = (config && config.params) || {}; + if (typeof params.pid !== 'string' || params.pid.length == 0) { + logError(`${MODULE_NAME}: submodule requires a publisher ID to be defined`); + return; + } + + const { pid, syncUrl = SYNC_URL } = params; + const url = constructUrl(syncUrl, { publisherId: pid }); + + return { + callback: (cb) => { + asyncRequest(url, cb); + } + } + }, + + eids: { + 'operaId': { + source: 't.adx.opera.com', + atype: 1 + }, + } +}; + +submodule('userId', operaIdSubmodule); diff --git a/modules/operaadsIdSystem.md b/modules/operaadsIdSystem.md new file mode 100644 index 00000000000..288fb960b96 --- /dev/null +++ b/modules/operaadsIdSystem.md @@ -0,0 +1,52 @@ +# Opera ID System + +For help adding this module, please contact [adtech-prebid-group@opera.com](adtech-prebid-group@opera.com). + +### Prebid Configuration + +You should configure this module under your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "operaId", + storage: { + name: "operaId", + type: "html5", + expires: 14 + }, + params: { + pid: "your-pulisher-ID-here" + } + } + ] + } +}) +``` +
+ +| Param under `userSync.userIds[]` | Scope | Type | Description | Example | +| -------------------------------- | -------- | ------ | ----------------------------- | ----------------------------------------- | +| name | Required | string | ID for the operaId module | `"operaId"` | +| storage | Optional | Object | Settings for operaId storage | See [storage settings](#storage-settings) | +| params | Required | Object | Parameters for opreaId module | See [params](#params) | +
+ +### Params + +| Param under `params` | Scope | Type | Description | Example | +| -------------------- | -------- | ------ | ------------------------------ | --------------- | +| pid | Required | string | Publisher ID assigned by Opera | `"pub12345678"` | +
+ +### Storage Settings + +The following settings are suggested for the `storage` property in the `userSync.userIds[]` object: + +| Param under `storage` | Type | Description | Example | +| --------------------- | ------------- | -------------------------------------------------------------------------------- | ----------- | +| name | String | Where the ID will be stored | `"operaId"` | +| type | String | For best performance, this should be `"html5"` | `"html5"` | +| expires | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | \ No newline at end of file diff --git a/modules/opscoBidAdapter.js b/modules/opscoBidAdapter.js new file mode 100644 index 00000000000..87d00f14de0 --- /dev/null +++ b/modules/opscoBidAdapter.js @@ -0,0 +1,129 @@ +import {deepAccess, deepSetValue, isArray, logInfo} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ENDPOINT = 'https://exchange.ops.co/openrtb2/auction'; +const BIDDER_CODE = 'opsco'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => !!(bid.params && + bid.params.placementId && + bid.params.publisherId && + bid.mediaTypes?.banner?.sizes && + Array.isArray(bid.mediaTypes?.banner?.sizes)), + + buildRequests: (validBidRequests, bidderRequest) => { + const {publisherId, placementId, siteId} = validBidRequests[0].params; + + const payload = { + id: bidderRequest.bidderRequestId, + imp: validBidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: {format: extractSizes(bidRequest)}, + ext: { + opsco: { + placementId: placementId, + publisherId: publisherId, + } + } + })), + site: { + id: siteId, + publisher: {id: publisherId}, + domain: bidderRequest.refererInfo?.domain, + page: bidderRequest.refererInfo?.page, + ref: bidderRequest.refererInfo?.ref, + }, + }; + + if (isTest(validBidRequests[0])) { + payload.test = 1; + } + + if (bidderRequest.gdprConsent) { + deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); + if (eids && eids.length !== 0) { + deepSetValue(payload, 'user.ext.eids', eids); + } + + const schainData = deepAccess(validBidRequests[0], 'schain.nodes'); + if (isArray(schainData) && schainData.length > 0) { + deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); + } + + if (bidderRequest.uspConsent) { + deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload), + }; + }, + + interpretResponse: (serverResponse) => { + const response = (serverResponse || {}).body; + const bidResponses = response?.seatbid?.[0]?.bid?.map(bid => ({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: DEFAULT_NET_REVENUE, + currency: DEFAULT_CURRENCY, + meta: {advertiserDomains: bid?.adomain || []}, + mediaType: bid.mediaType || bid.mtype + })) || []; + + if (!bidResponses.length) { + logInfo('opsco.interpretResponse :: No valid responses'); + } + + return bidResponses; + }, + + getUserSyncs: (syncOptions, serverResponses) => { + logInfo('opsco.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return []; + } + let syncs = []; + serverResponses.forEach(resp => { + const userSync = deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + const syncDetails = Object.values(userSync).flatMap(value => value.syncs || []); + syncDetails.forEach(syncDetail => { + const type = syncDetail.type === 'iframe' ? 'iframe' : 'image'; + if ((type === 'iframe' && syncOptions.iframeEnabled) || (type === 'image' && syncOptions.pixelEnabled)) { + syncs.push({type, url: syncDetail.url}); + } + }); + } + }); + + logInfo('opsco.getUserSyncs result=%o', syncs); + return syncs; + } +}; + +function extractSizes(bidRequest) { + return (bidRequest.mediaTypes?.banner?.sizes || []).map(([width, height]) => ({w: width, h: height})); +} + +function isTest(validBidRequest) { + return validBidRequest.params?.test === true; +} + +registerBidder(spec); diff --git a/modules/opscoBidAdapter.md b/modules/opscoBidAdapter.md new file mode 100644 index 00000000000..b5e1015a325 --- /dev/null +++ b/modules/opscoBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Opsco Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@ops.co +``` + +# Description + +Module that connects to Opscos's demand sources. + +# Test Parameters + +## Banner + +``` +var adUnits = [ + { + code: 'test-ad', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'opsco', + params: { + placementId: '1234', + publisherId: '9876', + test: true + } + }], + } +]; +``` diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js index a0fa641a424..27b858c84fe 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -1,23 +1,34 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { deepAccess, parseSizesInput, getAdUnitSizes } from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {deepAccess, parseSizesInput} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'optidigital'; const GVL_ID = 915; const ENDPOINT_URL = 'https://pbs.optidigital.com/bidder'; const USER_SYNC_URL_IFRAME = 'https://scripts.opti-digital.com/js/presync.html?endpoint=optidigital'; let CUR = 'USD'; +let isSynced = false; export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, 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. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { let isValid = false; if (typeof bid.params !== 'undefined' && bid.params.placementId && bid.params.publisherId) { @@ -27,11 +38,11 @@ export const spec = { return isValid; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { if (!validBidRequests || validBidRequests.length === 0 || !bidderRequest || !bidderRequest.bids) { return []; @@ -46,8 +57,6 @@ export const spec = { referrer: (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : '', hb_version: '$prebid.version$', deviceWidth: document.documentElement.clientWidth, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: deepAccess(validBidRequests[0], 'auctionId'), bidderRequestId: deepAccess(validBidRequests[0], 'bidderRequestId'), publisherId: deepAccess(validBidRequests[0], 'params.publisherId'), imp: validBidRequests.map(bidRequest => buildImp(bidRequest, ortb2)), @@ -56,6 +65,10 @@ export const spec = { bapp: deepAccess(validBidRequests[0], 'params.bapp') || [] } + if (validBidRequests[0].auctionId) { + payload.auctionId = validBidRequests[0].auctionId; + } + if (validBidRequests[0].params.pageTemplate && validBidRequests[0].params.pageTemplate !== '') { payload.pageTemplate = validBidRequests[0].params.pageTemplate; } @@ -87,6 +100,12 @@ export const spec = { payload.uspConsent = bidderRequest.uspConsent; } + if (_getEids(validBidRequests[0])) { + payload.user = { + eids: _getEids(validBidRequests[0]) + } + } + const payloadObject = JSON.stringify(payload); return { method: 'POST', @@ -95,11 +114,11 @@ export const spec = { }; }, /** - * 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. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { const bidResponses = []; serverResponse = serverResponse.body; @@ -128,29 +147,31 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncurl = ''; + if (!isSynced) { + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&ccpa_consent=${uspConsent.consentString}`; + } - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - if (uspConsent && uspConsent.consentString) { - syncurl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: USER_SYNC_URL_IFRAME + syncurl - }]; + if (syncOptions.iframeEnabled) { + isSynced = true; + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } } }, }; @@ -218,4 +239,14 @@ function _getFloor (bid, sizes, currency) { return floor !== null ? floor : bid.params.floor; } +function _getEids(bidRequest) { + if (deepAccess(bidRequest, 'userIdAsEids')) { + return bidRequest.userIdAsEids; + } +} + +export function resetSync() { + isSynced = false; +} + registerBidder(spec); diff --git a/modules/optidigitalBidAdapter.md b/modules/optidigitalBidAdapter.md index 466dfb3bef2..327e7a27c75 100755 --- a/modules/optidigitalBidAdapter.md +++ b/modules/optidigitalBidAdapter.md @@ -37,10 +37,13 @@ Bidder Adapter for Prebid.js. ``` pbjs.setConfig({ - userSync: { - iframeEnabled: true, - syncEnabled: true, - syncDelay: 3000 - } +  userSync: { +    filterSettings: { +      iframe: { +        bidders: '*', // '*' represents all bidders +        filter: 'include' +      } +    } +  } }); ``` diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index dfe8f1bfcf2..bd564e3a260 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -16,12 +16,17 @@ * @property {string} clientID * @property {string} optimeraKeyName * @property {string} device + * @property {string} apiVersion */ import { logInfo, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajaxBuilder } from '../src/ajax.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** @type {ModuleParams} */ let _moduleParams = {}; @@ -29,7 +34,8 @@ let _moduleParams = {}; * Default Optimera Key Name * This can default to hb_deal_optimera for publishers * who used the previous Optimera Bidder Adapter. - * @type {string} */ + * @type {string} + */ export let optimeraKeyName = 'hb_deal_optimera'; /** @@ -38,7 +44,10 @@ export let optimeraKeyName = 'hb_deal_optimera'; * the targeting values. * @type {string} */ -export const scoresBaseURL = 'https://dyv1bugovvq1g.cloudfront.net/'; +export const scoresBaseURL = { + v0: 'https://dyv1bugovvq1g.cloudfront.net/', + v1: 'https://v1.oapi26b.com/', +}; /** * Optimera Score File URL. @@ -58,6 +67,12 @@ export let clientID; */ export let device = 'default'; +/** + * Optional apiVersion parameter. + * @type {string} + */ +export let apiVersion = 'v0'; + /** * Targeting object for all ad positions. * @type {string} @@ -127,6 +142,7 @@ export function onAuctionInit(auctionDetails, config, userConsent) { /** * Initialize the Module. + * moduleConfig.params.apiVersion can be either v0 or v1. */ export function init(moduleConfig) { _moduleParams = moduleConfig.params; @@ -138,6 +154,9 @@ export function init(moduleConfig) { if (_moduleParams.device) { device = _moduleParams.device; } + if (_moduleParams.apiVersion) { + apiVersion = (_moduleParams.apiVersion.includes('v1', 'v0')) ? _moduleParams.apiVersion : 'v0'; + } setScoresURL(); scoreFileRequest(); return true; @@ -162,7 +181,15 @@ export function init(moduleConfig) { export function setScoresURL() { const optimeraHost = window.location.host; const optimeraPathName = window.location.pathname; - const newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; + const baseUrl = scoresBaseURL[apiVersion] ? scoresBaseURL[apiVersion] : scoresBaseURL.v0; + let newScoresURL; + + if (apiVersion === 'v1') { + newScoresURL = `${baseUrl}api/products/scores?c=${clientID}&h=${optimeraHost}&p=${optimeraPathName}&s=${device}`; + } else { + newScoresURL = `${baseUrl}${clientID}/${optimeraHost}${optimeraPathName}.js`; + } + if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; fetchScoreFile = true; diff --git a/modules/optimeraRtdProvider.md b/modules/optimeraRtdProvider.md index 610dec537e0..8b66deb5ad5 100644 --- a/modules/optimeraRtdProvider.md +++ b/modules/optimeraRtdProvider.md @@ -1,6 +1,6 @@ # Overview ``` -Module Name: Optimera Real Time Date Module +Module Name: Optimera Real Time Data Module Module Type: RTD Module Maintainer: mcallari@optimera.nyc ``` @@ -26,7 +26,8 @@ Configuration example for using RTD module with `optimera` provider params: { clientID: '9999', optimeraKeyName: 'optimera', - device: 'de' + device: 'de', + apiVersion: 'v0', } } ] @@ -42,3 +43,4 @@ Contact Optimera to get assistance with the params. | clientID | string | required | Optimera Client ID | | optimeraKeyName | string | optional | GAM key name for Optimera. If migrating from the Optimera bidder adapter this will default to hb_deal_optimera and can be ommitted from the configuration. | | device | string | optional | Device type code for mobile, tablet, or desktop. Either mo, tb, de | +| apiVersion | string | optional | Optimera API Versions. Either v0, or v1. ** Note: v1 wll need to be enabled specifically for your account, otherwise use v0. \ No newline at end of file diff --git a/modules/optimonAnalyticsAdapter.js b/modules/optimonAnalyticsAdapter.js index 82bc18f605d..68baf007563 100644 --- a/modules/optimonAnalyticsAdapter.js +++ b/modules/optimonAnalyticsAdapter.js @@ -1,12 +1,12 @@ /** -* -********************************************************* -* -* Optimon.io Prebid Analytics Adapter -* -********************************************************* -* -*/ + * + ********************************************************* + * + * Optimon.io Prebid Analytics Adapter + * + ********************************************************* + * + */ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index f135ebb2bd1..0f912384db7 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,11 +1,15 @@ import { isFn, isPlainObject } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { getGlobal } from '../src/prebidGlobal.js'; -const storageManager = getStorageManager({bidderCode: 'orbidder'}); +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ +const storageManager = getStorageManager({ bidderCode: 'orbidder' }); /** * Determines whether or not the given bid response is valid. @@ -69,7 +73,7 @@ export const spec = { return !!(bid.sizes && bid.bidId && bid.params && (bid.params.accountId && (typeof bid.params.accountId === 'string')) && (bid.params.placementId && (typeof bid.params.placementId === 'string')) && - ((typeof bid.params.profile === 'undefined') || (typeof bid.params.profile === 'object'))); + ((typeof bid.params.keyValues === 'undefined') || (typeof bid.params.keyValues === 'object'))); }, /** @@ -99,15 +103,7 @@ export const spec = { data: { v: getGlobal().version, pageUrl: referer, - bidId: bidRequest.bidId, - auctionId: bidRequest.auctionId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - transactionId: bidRequest.ortb2Imp?.ext?.tid, - adUnitCode: bidRequest.adUnitCode, - bidRequestCount: bidRequest.bidRequestCount, - params: bidRequest.params, - sizes: bidRequest.sizes, - mediaTypes: bidRequest.mediaTypes + ...bidRequest // get all data provided by bid request } }; diff --git a/modules/orbitsoftBidAdapter.js b/modules/orbitsoftBidAdapter.js index 4c3f2e38c58..f55c7ff9917 100644 --- a/modules/orbitsoftBidAdapter.js +++ b/modules/orbitsoftBidAdapter.js @@ -1,5 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'orbitsoft'; let styleParamsMap = { @@ -45,10 +46,10 @@ export const spec = { for (let i = 0; i < validBidRequests.length; i++) { bidRequest = validBidRequests[i]; let bidRequestParams = bidRequest.params; - let placementId = utils.getBidIdParameter('placementId', bidRequestParams); - let requestUrl = utils.getBidIdParameter('requestUrl', bidRequestParams); - let referrer = utils.getBidIdParameter('ref', bidRequestParams); - let location = utils.getBidIdParameter('loc', bidRequestParams); + let placementId = getBidIdParameter('placementId', bidRequestParams); + let requestUrl = getBidIdParameter('requestUrl', bidRequestParams); + let referrer = getBidIdParameter('ref', bidRequestParams); + let location = getBidIdParameter('loc', bidRequestParams); // Append location & referrer if (location === '') { location = utils.getWindowLocation(); @@ -58,7 +59,7 @@ export const spec = { } // Styles params - let stylesParams = utils.getBidIdParameter('style', bidRequestParams); + let stylesParams = getBidIdParameter('style', bidRequestParams); let stylesParamsArray = {}; for (let currentValue in stylesParams) { if (stylesParams.hasOwnProperty(currentValue)) { @@ -74,7 +75,7 @@ export const spec = { } } // Custom params - let customParams = utils.getBidIdParameter('customParams', bidRequestParams); + let customParams = getBidIdParameter('customParams', bidRequestParams); let customParamsArray = {}; for (let customField in customParams) { if (customParams.hasOwnProperty(customField)) { diff --git a/modules/otmBidAdapter.js b/modules/otmBidAdapter.js index 6125cee6593..7d4049e3054 100644 --- a/modules/otmBidAdapter.js +++ b/modules/otmBidAdapter.js @@ -2,14 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { logInfo, logError, - getBidIdParameter, _each, getValue, isFn, isPlainObject, isArray, isStr, - isNumber, + isNumber, getBidIdParameter, } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 0637d680912..6015ff37e08 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; import {OUTSTREAM} from '../src/video.js'; import {_map, deepAccess, deepSetValue, isArray, logWarn, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; @@ -23,6 +24,9 @@ const NATIVE_PARAMS = { cta: { id: 1, type: 12, name: 'data' } }; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const OB_USER_TOKEN_KEY = 'OB-USER-TOKEN'; + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -130,6 +134,11 @@ export const spec = { request.test = 1; } + const obUserToken = storage.getDataFromLocalStorage(OB_USER_TOKEN_KEY) + if (obUserToken) { + deepSetValue(request, 'user.ext.obusertoken', obUserToken) + } + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) @@ -140,6 +149,13 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) } + if (bidderRequest.gppConsent) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.gppConsent.gppString) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.gppConsent.applicableSections) + } else if (deepAccess(bidderRequest, 'ortb2.regs.gpp')) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.ortb2.regs.gpp) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.ortb2.regs.gpp_sid) + } if (eids) { deepSetValue(request, 'user.ext.eids', eids); @@ -203,7 +219,7 @@ export const spec = { } }).filter(Boolean); }, - getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { const syncs = []; let syncUrl = config.getConfig('outbrain.usersyncUrl'); @@ -216,6 +232,10 @@ export const spec = { if (uspConsent) { query.push('us_privacy=' + encodeURIComponent(uspConsent)); } + if (gppConsent) { + query.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + query.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','))); + } syncs.push({ type: 'image', diff --git a/modules/oxxionAnalyticsAdapter.js b/modules/oxxionAnalyticsAdapter.js index 737d130ac6c..25732d440ff 100644 --- a/modules/oxxionAnalyticsAdapter.js +++ b/modules/oxxionAnalyticsAdapter.js @@ -21,8 +21,9 @@ let saveEvents = {} let allEvents = {} let auctionEnd = {} let initOptions = {} +let mode = {}; let endpoint = 'https://default' -let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId']; +let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId', 'ova']; function getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; @@ -41,16 +42,27 @@ function filterAttributes(arg, removead) { } if (typeof arg['gdprConsent'] != 'undefined') { response['gdprConsent'] = {}; - if (typeof arg['gdprConsent']['consentString'] != 'undefined') { response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; } + if (typeof arg['gdprConsent']['consentString'] != 'undefined') { + response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; + } } - if (typeof arg['meta'] == 'object' && typeof arg['meta']['advertiserDomains'] != 'undefined') { - response['meta'] = {'advertiserDomains': arg['meta']['advertiserDomains']}; + if (typeof arg['meta'] == 'object') { + response['meta'] = {}; + if (typeof arg['meta']['advertiserDomains'] != 'undefined') { + response['meta']['advertiserDomains'] = arg['meta']['advertiserDomains']; + } + if (typeof arg['meta']['demandSource'] == 'string') { + response['meta']['demandSource'] = arg['meta']['demandSource']; + } } requestsAttributes.forEach((attr) => { if (typeof arg[attr] != 'undefined') { response[attr] = arg[attr]; } }); - if (typeof response['creativeId'] == 'number') { response['creativeId'] = response['creativeId'].toString(); } + if (typeof response['creativeId'] == 'number') { + response['creativeId'] = response['creativeId'].toString(); + } } + response['oxxionMode'] = mode; return response; } @@ -171,6 +183,15 @@ function handleBidWon(args) { } }); } + if (auction['auctionId'] == args['auctionId'] && typeof auction['bidderRequests'] == 'object') { + auction['bidderRequests'].forEach((req) => { + req.bids.forEach((bid) => { + if (bid['bidId'] == args['requestId'] && bid['transactionId'] == args['transactionId']) { + args['ova'] = bid['ova']; + } + }); + }); + } }); } args['cpmIncrement'] = increment; @@ -220,7 +241,8 @@ let oxxionAnalytics = Object.assign(adapter({url, analyticsType}), { addTimeout(args); break; } - }}); + } +}); // save the base class function oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; @@ -229,7 +251,12 @@ oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; oxxionAnalytics.enableAnalytics = function (config) { oxxionAnalytics.originEnableAnalytics(config); // call the base class function initOptions = config.options; - if (initOptions.domain) { endpoint = 'https://' + initOptions.domain; } + if (initOptions.domain) { + endpoint = 'https://' + initOptions.domain; + } + if (window.OXXION_MODE) { + mode = window.OXXION_MODE; + } }; adapterManager.registerAnalyticsAdapter({ diff --git a/modules/oxxionRtdProvider.js b/modules/oxxionRtdProvider.js index c6f8b9a902b..a0476d8ca0f 100644 --- a/modules/oxxionRtdProvider.js +++ b/modules/oxxionRtdProvider.js @@ -1,12 +1,14 @@ import { submodule } from '../src/hook.js' -import { deepAccess, logInfo, logError } from '../src/utils.js' +import { logInfo, logError } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import adapterManager from '../src/adapterManager.js'; -const oxxionRtdSearchFor = [ 'adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'userId', 'labelAny', 'adId' ]; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const LOG_PREFIX = 'oxxionRtdProvider submodule: '; -const allAdUnits = []; const bidderAliasRegistry = adapterManager.aliasRegistry || {}; /** @type {RtdSubmodule} */ @@ -14,19 +16,18 @@ export const oxxionSubmodule = { name: 'oxxionRtd', init: init, getBidRequestData: getAdUnits, - onBidResponseEvent: insertVideoTracking, getRequestsList: getRequestsList, getFilteredAdUnitsOnBidRates: getFilteredAdUnitsOnBidRates, }; function init(config, userConsent) { if (!config.params || !config.params.domain) { return false } - if (config.params.contexts && Array.isArray(config.params.contexts) && config.params.contexts.length > 0) { return true; } if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { return true } return false; } function getAdUnits(reqBidsConfigObj, callback, config, userConsent) { + const moduleStarted = new Date(); logInfo(LOG_PREFIX + 'started with ', config); if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { let filteredBids; @@ -51,83 +52,13 @@ function getAdUnits(reqBidsConfigObj, callback, config, userConsent) { }); } if (typeof callback == 'function') { callback(); } - }).catch(error => logError(LOG_PREFIX, 'bidInterestError', error)); - } - if (config.params.contexts && Array.isArray(config.params.contexts) && config.params.contexts.length > 0) { - const reqAdUnits = reqBidsConfigObj.adUnits; - if (Array.isArray(reqAdUnits)) { - reqAdUnits.forEach(adunit => { - if (config.params.contexts.includes(deepAccess(adunit, 'mediaTypes.video.context'))) { - allAdUnits.push(adunit); - } - }); - } - if (!(typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') && typeof callback == 'function') { - callback(); - } - } -} - -function insertVideoTracking(bidResponse, config, userConsent) { - // this should only be do for video bids - if (bidResponse.mediaType === 'video') { - let maxCpm = 0; - const trackingUrl = getImpUrl(config, bidResponse, maxCpm); - if (!trackingUrl) { - return; - } - // Vast Impression URL - if (bidResponse.vastUrl) { - bidResponse.vastImpUrl = bidResponse.vastImpUrl - ? trackingUrl + '&url=' + encodeURI(bidResponse.vastImpUrl) - : trackingUrl; - logInfo(LOG_PREFIX + 'insert into vastImpUrl for adId ' + bidResponse.adId); - } - // Vast XML document - if (bidResponse.vastXml !== undefined) { - const doc = new DOMParser().parseFromString(bidResponse.vastXml, 'text/xml'); - const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine'); - let hasAltered = false; - if (wrappers.length) { - wrappers.forEach(wrapper => { - const impression = doc.createElement('Impression'); - impression.appendChild(doc.createCDATASection(trackingUrl)); - wrapper.appendChild(impression) - }); - bidResponse.vastXml = new XMLSerializer().serializeToString(doc); - hasAltered = true; - } - if (hasAltered) { - logInfo(LOG_PREFIX + 'insert into vastXml for adId ' + bidResponse.adId); + const timeToRun = new Date() - moduleStarted; + logInfo(LOG_PREFIX + ' time to run: ' + timeToRun); + if (getRandomNumber(50) == 1) { + ajax('https://' + config.params.domain + '.oxxion.io/ova/time', null, JSON.stringify({'duration': timeToRun, 'auctionId': reqBidsConfigObj.auctionId}), {method: 'POST', withCredentials: true}); } - } - } -} - -function getImpUrl(config, data, maxCpm) { - const adUnitCode = data.adUnitCode; - const adUnits = allAdUnits.find(adunit => adunit.code === adUnitCode && - 'mediaTypes' in adunit && - 'video' in adunit.mediaTypes && - typeof adunit.mediaTypes.video.context === 'string'); - const context = adUnits !== undefined - ? adUnits.mediaTypes.video.context - : 'unknown'; - if (!config.params.contexts.includes(context)) { - return false; + }).catch(error => logError(LOG_PREFIX, 'bidInterestError', error)); } - let trackingImpUrl = 'https://' + config.params.domain + '.oxxion.io/analytics/vast_imp?'; - trackingImpUrl += oxxionRtdSearchFor.reduce((acc, param) => { - switch (typeof data[param]) { - case 'string': - case 'number': - acc += param + '=' + data[param] + '&' - break; - } - return acc; - }, ''); - const cpmIncrement = 0.0; - return trackingImpUrl + 'cpmIncrement=' + cpmIncrement + '&context=' + context; } function getPromisifiedAjax (url, data = {}, options = {}) { @@ -146,22 +77,27 @@ function getPromisifiedAjax (url, data = {}, options = {}) { function getFilteredAdUnitsOnBidRates (bidsRateInterests, adUnits, params, useSampling) { const { threshold, samplingRate } = params; + const sampling = getRandomNumber(100) < samplingRate && useSampling; const filteredBids = []; // Separate bidsRateInterests in two groups against threshold & samplingRate - const { interestingBidsRates, uninterestingBidsRates } = bidsRateInterests.reduce((acc, interestingBid) => { + const { interestingBidsRates, uninterestingBidsRates, sampledBidsRates } = bidsRateInterests.reduce((acc, interestingBid) => { const isBidRateUpper = typeof threshold == 'number' ? interestingBid.rate === true || interestingBid.rate > threshold : interestingBid.suggestion; - const isBidInteresting = isBidRateUpper || (getRandomNumber(100) < samplingRate && useSampling); + const isBidInteresting = isBidRateUpper || sampling; const key = isBidInteresting ? 'interestingBidsRates' : 'uninterestingBidsRates'; acc[key].push(interestingBid); + if (!isBidRateUpper && sampling) { + acc['sampledBidsRates'].push(interestingBid); + } return acc; }, { interestingBidsRates: [], - uninterestingBidsRates: [] // Do something with later + uninterestingBidsRates: [], // Do something with later + sampledBidsRates: [] }); logInfo(LOG_PREFIX, 'getFilteredAdUnitsOnBidRates()', interestingBidsRates, uninterestingBidsRates); // Filter bids and adUnits against interesting bids rates const newAdUnits = adUnits.filter(({ bids = [] }, adUnitIndex) => { - adUnits[adUnitIndex].bids = bids.filter(bid => { + adUnits[adUnitIndex].bids = bids.filter((bid, bidIndex) => { if (!params.bidders || params.bidders.includes(bid.bidder)) { const index = interestingBidsRates.findIndex(({ id }) => id === bid._id); if (index == -1) { @@ -173,10 +109,19 @@ function getFilteredAdUnitsOnBidRates (bidsRateInterests, adUnits, params, useSa delete tmpBid.floorData; } filteredBids.push(tmpBid); + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'filtered'; + } else { + if (sampledBidsRates.findIndex(({ id }) => id === bid._id) == -1) { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'cleared'; + } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'sampled'; + logInfo(LOG_PREFIX + ' sampled ! '); + } } delete bid._id; return index !== -1; } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'protected'; return true; } }); diff --git a/modules/oxxionRtdProvider.md b/modules/oxxionRtdProvider.md index 14b4abec5c2..bfdbfae1fa9 100644 --- a/modules/oxxionRtdProvider.md +++ b/modules/oxxionRtdProvider.md @@ -7,7 +7,7 @@ Maintainer: tech@oxxion.io # Oxxion Real-Time-Data submodule Oxxion helps you to understand how your prebid stack performs. -This Rtd module is to use in order to improve video events tracking and/or to filter bidder requested. +This Rtd module purpose is to filter bidders requested. # Integration @@ -30,7 +30,6 @@ pbjs.setConfig( waitForIt: true, params: { domain: "test.endpoint", - contexts: ["instream"], threshold: false, samplingRate: 10, } @@ -47,12 +46,6 @@ pbjs.setConfig( |:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------| | domain | String | This string identifies yourself in Oxxion's systems and is provided to you by your Oxxion representative. | -# setConfig Parameters for Video Tracking - -| Name | Type | Description | -|:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------| -| contexts | Array | Array defining which video contexts to add tracking events into. Values can be instream and/or outstream. | - # setConfig Parameters for bidder filtering | Name | Type | Description | diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 1b725ce3a05..0d921f57cda 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -7,7 +7,8 @@ import { isArray, contains, mergeDeep, - parseUrl + parseUrl, + generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; @@ -16,15 +17,15 @@ import {getPriceBucketString} from '../src/cpmBucketManager.js'; import { Renderer } from '../src/Renderer.js'; import {getRefererInfo} from '../src/refererDetection.js'; const BIDDER_CODE = 'ozone'; -const ORIGIN = 'https://elb.the-ozone-project.com'; // applies only to auction & cookie +const ORIGIN = 'https://elb.the-ozone-project.com' // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const OZONECOOKIESYNC = '/static/load-cookie.html'; const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js'; const ORIGIN_DEV = 'https://test.ozpr.net'; -const OZONEVERSION = '2.8.0'; +const OZONEVERSION = '2.9.1'; export const spec = { gvlid: 524, - aliases: [{code: 'lmc', gvlid: 524}], + aliases: [{code: 'lmc', gvlid: 524}, {code: 'venatus', gvlid: 524}], version: OZONEVERSION, code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], @@ -36,7 +37,8 @@ export const spec = { 'keyPrefix': 'oz', 'auctionUrl': ORIGIN + AUCTIONURI, 'cookieSyncUrl': ORIGIN + OZONECOOKIESYNC, - 'rendererUrl': OZONE_RENDERER_URL + 'rendererUrl': OZONE_RENDERER_URL, + 'batchRequests': false /* you can change this to true OR override it in the config: config.ozone.batchRequests */ }, loadWhitelabelData(bid) { if (this.propertyBag.whitelabel) { return; } @@ -73,6 +75,12 @@ export const spec = { this.propertyBag.whitelabel.auctionUrl = bidderConfig.endpointOverride.auctionUrl; } } + if (bidderConfig.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = bidderConfig.batchRequests; + } + if (arr.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = true; + } try { if (arr.hasOwnProperty('auction') && arr.auction === 'dev') { logInfo('GET: auction=dev'); @@ -94,11 +102,15 @@ export const spec = { getRendererUrl() { return this.propertyBag.whitelabel.rendererUrl; }, + isBatchRequests() { + logInfo('isBatchRequests going to return ', this.propertyBag.whitelabel.batchRequests); + return this.propertyBag.whitelabel.batchRequests; + }, isBidRequestValid(bid) { this.loadWhitelabelData(bid); logInfo('isBidRequestValid : ', config.getConfig(), bid); let adUnitCode = bid.adUnitCode; // adunit[n].code - let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED'; + let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED' if (!(bid.params.hasOwnProperty('placementId'))) { logError(err1.replace('{param}', 'placementId'), adUnitCode); return false; @@ -199,7 +211,7 @@ export const spec = { let placementId = placementIdOverrideFromGetParam || this.getPlacementId(ozoneBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = ozoneBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder ozone made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().page); obj.secure = parsed.protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!ozoneBidRequest.hasOwnProperty('mediaTypes')) { @@ -262,9 +274,7 @@ export const spec = { obj.placementId = placementId; deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); obj.ext[whitelabelBidder] = {}; - // TODO: fix auctionId/transactionID leak: https://github.com/prebid/Prebid.js/issues/9781 obj.ext[whitelabelBidder].adUnitCode = ozoneBidRequest.adUnitCode; // eg. 'mpu' - obj.ext[whitelabelBidder].transactionId = ozoneBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (ozoneBidRequest.params.hasOwnProperty('customData')) { obj.ext[whitelabelBidder].customData = ozoneBidRequest.params.customData; } @@ -291,6 +301,10 @@ export const spec = { if (!schain && deepAccess(ozoneBidRequest, 'schain')) { schain = ozoneBidRequest.schain; } + let gpid = deepAccess(ozoneBidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + deepSetValue(obj, 'ext.gpid', gpid); + } return obj; }); let extObj = {}; @@ -326,7 +340,7 @@ export const spec = { let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module ozoneRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': getRefererInfo().page, + 'page': this.getRefererInfo().page, 'id': htmlParams.siteId }; ozoneRequest.test = config.getConfig('debug') ? 1 : 0; @@ -355,13 +369,33 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(ozoneRequest, 'regs.coppa', 1); } + let ozUuid = generateUUID(); + if (this.isBatchRequests()) { + logInfo('going to batch the requests'); + let arrRet = []; // return an array of objects containing data describing max 10 bids + for (let i = 0; i < tosendtags.length; i += 10) { + ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) + ozoneRequest.imp = tosendtags.slice(i, i + 10); + ozoneRequest.ext = extObj; + deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); + if (ozoneRequest.imp.length > 0) { + arrRet.push({ + method: 'POST', + url: this.getAuctionUrl(), + data: JSON.stringify(ozoneRequest), + bidderRequest: bidderRequest + }); + } + } + logInfo('batch request going to return : ', arrRet); + return arrRet; + } + logInfo('requests will not be batched.'); if (singleRequest) { logInfo('buildRequests starting to generate response for a single request'); - ozoneRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequest.imp = tosendtags; ozoneRequest.ext = extObj; - deepSetValue(ozoneRequest, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', @@ -377,12 +411,9 @@ export const spec = { let arrRet = tosendtags.map(imp => { logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let ozoneRequestSingle = Object.assign({}, ozoneRequest); - imp.ext[whitelabelBidder].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - ozoneRequestSingle.id = imp.ext[whitelabelBidder].transactionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequestSingle.auctionId = imp.ext[whitelabelBidder].transactionId; // not sure if this should be here? + ozoneRequestSingle.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequestSingle.imp = [imp]; ozoneRequestSingle.ext = extObj; - deepSetValue(ozoneRequestSingle, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(ozoneRequestSingle, 'user.ext.eids', userExtEids); logInfo('buildRequests RequestSingle (for non-single) = ', ozoneRequestSingle); return { @@ -424,6 +455,7 @@ export const spec = { logInfo(`interpretResponse time: ${startTime} . Time between buildRequests done and interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; + let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { return []; } @@ -518,7 +550,7 @@ export const spec = { } } let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting[whitelabelPrefix + '_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting[whitelabelPrefix + '_auc_id'] = String(aucId); // was request.bidderRequest.auctionId adserverTargeting[whitelabelPrefix + '_winner'] = String(winningSeat); adserverTargeting[whitelabelPrefix + '_bid'] = 'true'; adserverTargeting[whitelabelPrefix + '_cache_id'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_id', 'no-id'); @@ -686,10 +718,29 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().location); logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, + getRefererInfo() { + if (getRefererInfo().hasOwnProperty('location')) { + logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); + return getRefererInfo(); + } else { + logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); + try { + return { + page: top.location.href, + location: top.location.href + }; + } catch (e) { + return { + page: window.location.href, + location: window.location.href + }; + } + } + }, blockTheRequest() { let ozRequest = this.getWhitelabelConfigItem('ozone.oz_request'); if (typeof ozRequest == 'boolean' && !ozRequest) { diff --git a/modules/ozoneBidAdapter.md b/modules/ozoneBidAdapter.md index 6f4cf752f22..9787e069283 100644 --- a/modules/ozoneBidAdapter.md +++ b/modules/ozoneBidAdapter.md @@ -73,6 +73,8 @@ adUnits = [{ }]; ``` + +``` //Instream Video adUnit adUnits = [{ @@ -108,4 +110,4 @@ adUnits = [{ } }] }; -``` +``` \ No newline at end of file diff --git a/modules/paapi.js b/modules/paapi.js new file mode 100644 index 00000000000..720935bd3f5 --- /dev/null +++ b/modules/paapi.js @@ -0,0 +1,241 @@ +/** + * Collect PAAPI component auction configs from bid adapters and make them available through `pbjs.getPAAPIConfig()` + */ +import {config} from '../src/config.js'; +import {getHook, module} from '../src/hook.js'; +import {deepSetValue, logInfo, logWarn, mergeDeep} from '../src/utils.js'; +import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; +import {currencyCompare} from '../libraries/currencyUtils/currency.js'; +import {maximum, minimum} from '../src/utils/reducers.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + +const MODULE = 'PAAPI'; + +const submodules = []; +const USED = new WeakSet(); + +export function registerSubmodule(submod) { + submodules.push(submod); + submod.init && submod.init({getPAAPIConfig}); +} + +module('paapi', registerSubmodule); + +function auctionConfigs() { + const store = new WeakMap(); + return function (auctionId, init = {}) { + const auction = auctionManager.index.getAuction({auctionId}); + if (auction == null) return; + if (!store.has(auction)) { + store.set(auction, init); + } + return store.get(auction); + }; +} + +const pendingForAuction = auctionConfigs(); +const configsForAuction = auctionConfigs(); +let latestAuctionForAdUnit = {}; +let moduleConfig = {}; + +['paapi', 'fledgeForGpt'].forEach(ns => { + config.getConfig(ns, config => { + init(config[ns], ns); + }); +}); + +export function reset() { + submodules.splice(0, submodules.length); + latestAuctionForAdUnit = {}; +} + +export function init(cfg, configNamespace) { + if (configNamespace !== 'paapi') { + logWarn(`'${configNamespace}' configuration options will be renamed to 'paapi'; consider using setConfig({paapi: [...]}) instead`); + } + if (cfg && cfg.enabled === true) { + moduleConfig = cfg; + logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} runAdAuction)`, cfg); + } else { + moduleConfig = {}; + logInfo(`${MODULE} disabled`, cfg); + } +} + +getHook('addComponentAuction').before(addComponentAuctionHook); +getHook('makeBidRequests').after(markForFledge); +events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); + +function getSlotSignals(bidsReceived = [], bidRequests = []) { + let bidfloor, bidfloorcur; + if (bidsReceived.length > 0) { + const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); + bidfloor = bestBid.cpm; + bidfloorcur = bestBid.currency; + } else { + const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f); + const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency]))); + bidfloor = minFloor?.floor; + bidfloorcur = minFloor?.currency; + } + const cfg = {}; + if (bidfloor) { + deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor); + bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur); + } + return cfg; +} + +function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes}) { + const allReqs = bidderRequests?.flatMap(br => br.bids); + const paapiConfigs = {}; + (adUnitCodes || []).forEach(au => { + paapiConfigs[au] = null; + !latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null); + }) + Object.entries(pendingForAuction(auctionId) || {}).forEach(([adUnitCode, auctionConfigs]) => { + const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; + const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); + paapiConfigs[adUnitCode] = { + componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)) + }; + latestAuctionForAdUnit[adUnitCode] = auctionId; + }); + configsForAuction(auctionId, paapiConfigs); + submodules.forEach(submod => submod.onAuctionConfig?.( + auctionId, + paapiConfigs, + (adUnitCode) => paapiConfigs[adUnitCode] != null && USED.add(paapiConfigs[adUnitCode])) + ); +} + +function setFPDSignals(auctionConfig, fpd) { + auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); +} + +export function addComponentAuctionHook(next, request, componentAuctionConfig) { + if (getFledgeConfig().enabled) { + const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; + const configs = pendingForAuction(auctionId); + if (configs != null) { + setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); + !configs.hasOwnProperty(adUnitCode) && (configs[adUnitCode] = []); + configs[adUnitCode].push(componentAuctionConfig); + } else { + logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig); + } + } + next(request, componentAuctionConfig); +} + +/** + * Get PAAPI auction configuration. + * + * @param auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used + * @param adUnitCode? optional ad unit filter + * @param includeBlanks if true, include null entries for ad units that match the given filters but do not have any available auction configs. + * @returns {{}} a map from ad unit code to auction config for the ad unit. + */ +export function getPAAPIConfig({auctionId, adUnitCode} = {}, includeBlanks = false) { + const output = {}; + const targetedAuctionConfigs = auctionId && configsForAuction(auctionId); + Object.keys((auctionId != null ? targetedAuctionConfigs : latestAuctionForAdUnit) ?? []).forEach(au => { + const latestAuctionId = latestAuctionForAdUnit[au]; + const auctionConfigs = targetedAuctionConfigs ?? (latestAuctionId && configsForAuction(latestAuctionId)); + if ((adUnitCode ?? au) === au) { + let candidate; + if (targetedAuctionConfigs?.hasOwnProperty(au)) { + candidate = targetedAuctionConfigs[au]; + } else if (auctionId == null && auctionConfigs?.hasOwnProperty(au)) { + candidate = auctionConfigs[au]; + } + if (candidate && !USED.has(candidate)) { + output[au] = candidate; + USED.add(candidate); + } else if (includeBlanks) { + output[au] = null; + } + } + }) + return output; +} + +getGlobal().getPAAPIConfig = (filters) => getPAAPIConfig(filters); + +function isFledgeSupported() { + return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator; +} + +function getFledgeConfig() { + const bidder = config.getCurrentBidder(); + const useGlobalConfig = moduleConfig.enabled && (bidder == null || !moduleConfig.bidders?.length || moduleConfig.bidders?.includes(bidder)); + return { + enabled: config.getConfig('fledgeEnabled') ?? useGlobalConfig, + ae: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? moduleConfig.defaultForSlots : undefined) + }; +} + +export function markForFledge(next, bidderRequests) { + if (isFledgeSupported()) { + bidderRequests.forEach((bidderReq) => { + config.runWithBidder(bidderReq.bidderCode, () => { + const {enabled, ae} = getFledgeConfig(); + Object.assign(bidderReq, {fledgeEnabled: enabled}); + bidderReq.bids.forEach(bidReq => { + deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? ae); + }); + }); + }); + } + next(bidderRequests); +} + +export function setImpExtAe(imp, bidRequest, context) { + if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { + delete imp.ext?.ae; + } +} + +registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); + +// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up +// fledge response processing in two steps: first aggregate all the auction configs by their imp... + +export function parseExtPrebidFledge(response, ortbResponse, context) { + (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => { + const impCtx = context.impContext[cfg.impid]; + if (!impCtx?.imp?.ext?.ae) { + logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp); + } else { + impCtx.fledgeConfigs = impCtx.fledgeConfigs || []; + impCtx.fledgeConfigs.push(cfg); + } + }); +} + +registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]}); + +// ...then, make them available in the adapter's response. This is the client side version, for which the +// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]} + +export function setResponseFledgeConfigs(response, ortbResponse, context) { + const configs = Object.values(context.impContext) + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({ + bidId: impCtx.bidRequest.bidId, + config: cfg.config + }))); + if (configs.length > 0) { + response.fledgeAuctionConfigs = configs; + } +} + +registerOrtbProcessor({ + type: RESPONSE, + name: 'fledgeAuctionConfigs', + priority: -1, + fn: setResponseFledgeConfigs, + dialects: [PBS] +}); diff --git a/modules/pairIdSystem.js b/modules/pairIdSystem.js index 489b97d02e3..dbff4c6a402 100644 --- a/modules/pairIdSystem.js +++ b/modules/pairIdSystem.js @@ -10,6 +10,10 @@ import {getStorageManager} from '../src/storageManager.js' import { logInfo } from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + */ + const MODULE_NAME = 'pairId'; const PAIR_ID_KEY = 'pairId'; const DEFAULT_LIVERAMP_PAIR_ID_KEY = '_lr_pairId'; @@ -27,29 +31,29 @@ function pairIdFromCookie(key) { /** @type {Submodule} */ export const pairIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * used to specify vendor id - * @type {number} - */ + * used to specify vendor id + * @type {number} + */ gvlid: 755, /** - * decode the stored id value for passing to bid requests - * @function - * @param { string | undefined } value - * @returns {{pairId:string} | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { string | undefined } value + * @returns {{pairId:string} | undefined } + */ decode(value) { return value && Array.isArray(value) ? {'pairId': value} : undefined }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @returns {id: string | undefined } - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @returns {id: string | undefined } + */ getId(config) { const pairIdsString = pairIdFromLocalStorage(PAIR_ID_KEY) || pairIdFromCookie(PAIR_ID_KEY) let ids = [] diff --git a/modules/pangleBidAdapter.js b/modules/pangleBidAdapter.js new file mode 100644 index 00000000000..c22a44687a2 --- /dev/null +++ b/modules/pangleBidAdapter.js @@ -0,0 +1,178 @@ +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, generateUUID, timestamp, deepAccess } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +import { Renderer } from '../src/Renderer.js'; + +const BIDDER_CODE = 'pangle'; +const ENDPOINT = 'https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'; + +const OUTSTREAM_RENDERER_URL = 'https://sf16-static.i18n-pglstatp.com/obj/ad-pattern-sg/pangle/web/ads/video.js'; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const PANGLE_COOKIE = '_pangle_id'; +const COOKIE_EXP = 86400 * 1000 * 365 * 1; // 1 year +const MEDIA_TYPES = { + Banner: 1, + Video: 2 +}; + +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: BIDDER_CODE }) + +export function isValidUuid(uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getPangleCookieId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(PANGLE_COOKIE); + + if (!sid || !isValidUuid(sid)) { + sid = generateUUID(); + setPangleCookieId(sid); + } + + return sid; +} + +function setPangleCookieId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(timestamp() + COOKIE_EXP).toGMTString(); + + storage.setCookie(PANGLE_COOKIE, sid, expires); + } +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }); + const devicetype = spec.getDeviceType(navigator.userAgent); + deepSetValue(data, 'device.devicetype', devicetype); + if (bidderRequest.userId && typeof bidderRequest.userId === 'object') { + const pangleId = getPangleCookieId(); + // add pangle cookie + const _eids = data.user?.ext?.eids ?? []; + deepSetValue(data, 'user.ext.eids', [ + ..._eids, + { + source: document.location.host, + uids: [ + { + id: pangleId, + atype: 1, + }, + ], + }, + ]); + } + bidRequests.forEach((item, idx) => { + deepSetValue(data.imp[idx], 'ext.networkids', item.params); + deepSetValue(data.imp[idx], 'banner.api', [5]); + deepSetValue(data, 'test', item.params.test ?? 0) + }); + return { + method: 'POST', + url: ENDPOINT, + data, + options: { contentType: 'application/json', withCredentials: true } + } +} + +function isVideoBid(bid) { + return !!deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return !!deepAccess(bid, 'mediaTypes.banner'); +} + +function renderOutstream(bid) { + bid.renderer.push(() => { + window.outstreamPlayer({ bid, codeId: bid.adUnitCode }); + }); +} + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY, + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + let bidResponse; + if (bid.mtype === MEDIA_TYPES.Video) { + context.mediaType = VIDEO; + bidResponse = buildBidResponse(bid, context); + if (bidRequest.mediaTypes.video?.context === 'outstream') { + const renderer = Renderer.install({id: bid.bidId, url: OUTSTREAM_RENDERER_URL, adUnitCode: bid.adUnitCode}); + renderer.setRender(renderOutstream); + bidResponse.renderer = renderer; + } + } + if (bid.mtype === MEDIA_TYPES.Banner) { + context.mediaType = BANNER; + bidResponse = buildBidResponse(bid, context); + } + return bidResponse; + }, +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + getDeviceType: function (ua) { + if ( + /ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test( + ua.toLowerCase() + ) + ) { + return 5; // 'tablet' + } + if ( + /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( + ua.toLowerCase() + ) + ) { + return 4; // 'mobile' + } + return 2; // 'desktop' + }, + + isBidRequestValid: function (bid) { + return Boolean(bid.params.token); + }, + + buildRequests(bidRequests, bidderRequest) { + const reqArr = []; + const videoBids = bidRequests.filter((bid) => isVideoBid(bid)); + const bannerBids = bidRequests.filter((bid) => isBannerBid(bid)); + bannerBids.forEach((bid) => { + reqArr.push(createRequest([bid], bidderRequest, BANNER)); + }) + videoBids.forEach((bid) => { + reqArr.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return reqArr; + }, + + interpretResponse(response, request) { + const bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids; + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/pangleBidAdapter.md b/modules/pangleBidAdapter.md new file mode 100644 index 00000000000..8fc628dcc89 --- /dev/null +++ b/modules/pangleBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: pangle Bidder Adapter +Module Type: Bidder Adapter +Maintainer: + +# Description + +An adapter to get a bid from pangle DSP. + +# Test Parameters + +```javascript +var adUnits = [{ + // banner + code: 'test1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + bids: [{ + bidder: 'pangle', + params: { + token: 'aass', + appid: 612, + placementid: 123, + } + }] +}]; +``` diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 3e3488f72f3..5651bdf0434 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -26,6 +26,12 @@ import {uspDataHandler} from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; const PARRABLE_GVLID = 928; diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 697d7721205..5a63990f84f 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -12,6 +12,10 @@ import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safe import {includes} from '../src/polyfill.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'permutive' const logger = prefixLog('[PermutiveRTD]') diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js new file mode 100644 index 00000000000..f3062fa4ff0 --- /dev/null +++ b/modules/pgamsspBidAdapter.js @@ -0,0 +1,230 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pgamssp'; +const AD_URL = 'https://us-east.pgammedia.com/pbjs'; +const SYNC_URL = 'https://cs.pgammedia.com'; + +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 || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor, + eids: [] + }; + + if (bid.userId) { + getUserId(placement.eids, bid.userId.uid2?.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.id5id?.uid, 'id5-sync.com'); + } + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + 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)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/pgamsspBidAdapter.md b/modules/pgamsspBidAdapter.md new file mode 100644 index 00000000000..c162ec33053 --- /dev/null +++ b/modules/pgamsspBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PGAMSSP Bidder Adapter +Module Type: PGAMSSP Bidder Adapter +Maintainer: info@pgammedia.com +``` + +# Description + +Connects to PGAMSSP exchange for bids. +PGAMSSP bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/pilotxBidAdapter.js b/modules/pilotxBidAdapter.js index 335c461e3d9..417c1f0c089 100644 --- a/modules/pilotxBidAdapter.js +++ b/modules/pilotxBidAdapter.js @@ -1,4 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'pilotx'; const ENDPOINT_URL = '//adn.pilotx.tv/hb' export const spec = { @@ -6,11 +14,11 @@ export const spec = { supportedMediaTypes: ['banner', 'video'], aliases: ['pilotx'], // short code /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function (bid) { let sizesCheck = !!bid.sizes let paramSizesCheck = !!bid.params.sizes @@ -35,11 +43,11 @@ export const spec = { return !!(bid.params.placementId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function (validBidRequests, bidderRequest) { let payloadItems = {}; validBidRequests.forEach(bidRequest => { @@ -84,11 +92,11 @@ export const spec = { }; }, /** - * 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. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function (serverResponse, bidRequest) { const serverBody = serverResponse.body; const bidResponses = []; @@ -135,7 +143,7 @@ export const spec = { /** * Formats placement ids for adserver ingestion purposes - * @param {string[]} The placement ID/s in an array + * @param {string[]} placementId the placement ID/s in an array */ setPlacementID: function (placementId) { if (Array.isArray(placementId)) { diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 608ba20aa5f..1c3f9b8da1a 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -3,10 +3,11 @@ import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find, includes} from '../src/polyfill.js'; -import {convertCamelToUnderscore, deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; import {auctionManager} from '../src/auctionManager.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const SOURCE = 'pbjs'; const storageManager = getStorageManager({bidderCode: 'pixfuture'}); diff --git a/modules/prebidServerBidAdapter/config.js b/modules/prebidServerBidAdapter/config.js index f6b8ac9f86a..87274504f64 100644 --- a/modules/prebidServerBidAdapter/config.js +++ b/modules/prebidServerBidAdapter/config.js @@ -1,11 +1,11 @@ // accountId and bidders params are not included here, should be configured by end-user export const S2S_VENDORS = { - 'appnexus': { + 'appnexuspsp': { adapter: 'prebidServer', enabled: true, endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' + p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', + noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }, syncEndpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', @@ -13,15 +13,6 @@ export const S2S_VENDORS = { }, timeout: 1000 }, - 'appnexuspsp': { - adapter: 'prebidServer', - enabled: true, - endpoint: { - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', - noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' - }, - timeout: 1000 - }, 'rubicon': { adapter: 'prebidServer', enabled: true, @@ -47,5 +38,14 @@ export const S2S_VENDORS = { noP1Consent: 'https://prebid.openx.net/cookie_sync' }, timeout: 1000 + }, + 'openwrap': { + adapter: 'prebidServer', + enabled: true, + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + timeout: 500 } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3cc38923d57..6e4aec8ad92 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,6 +1,6 @@ import Adapter from '../../src/adapter.js'; import { - bind, + deepAccess, deepClone, flatten, generateUUID, @@ -15,10 +15,9 @@ import { logWarn, triggerPixel, uniques, - deepAccess, } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager from '../../src/adapterManager.js'; +import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; @@ -29,6 +28,8 @@ import {hook} from '../../src/hook.js'; import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js'; import {useMetrics} from '../../src/utils/perfMetrics.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../src/activities/activities.js'; const getConfig = config.getConfig; @@ -206,7 +207,7 @@ getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); /** * resets the _synced variable back to false, primiarily used for testing purposes -*/ + */ export function resetSyncedStatus() { _syncCount = 0; } @@ -295,7 +296,7 @@ function doAllSyncs(bidders, s2sConfig) { // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { - doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); + doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, doAllSyncs.bind(null, bidders, s2sConfig), s2sConfig); } else { // bidder already has an ID, so just recurse to the next sync entry doAllSyncs(bidders, s2sConfig); @@ -354,8 +355,7 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { if (clientAdapter && clientAdapter.registerSyncs) { config.runWithBidder( bidder, - bind.call( - clientAdapter.registerSyncs, + clientAdapter.registerSyncs.bind( clientAdapter, [], gdprConsent, @@ -478,10 +478,14 @@ export function PrebidServer() { adapterMetrics }); } - done(); + done(false); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, - onError: done, + onError(msg, error) { + logError(`Prebid server call failed: '${msg}'`, error); + bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, {error, bidderRequest})); + done(error.timedOut); + }, onBid: function ({adUnit, bid}) { const metrics = bid.metrics = s2sBidRequest.metrics.fork().renameWith(); metrics.checkpoint('addBidResponse'); @@ -499,8 +503,8 @@ export function PrebidServer() { } } }, - onFledge: ({adUnitCode, config}) => { - addComponentAuction(adUnitCode, config); + onFledge: (params) => { + addComponentAuction({auctionId: bidRequests[0].auctionId, ...params}, params.config); } }) } @@ -571,7 +575,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, requestJson, - {contentType: 'text/plain', withCredentials: true} + { + contentType: 'text/plain', + withCredentials: true, + browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)) + } ); } else { logError('PBS request not made. Check endpoints.'); @@ -585,7 +593,7 @@ function shouldEmitNonbids(s2sConfig, response) { /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included - * @param {array} newEidPermissions + * @param {Array} newEidPermissions */ function setEidPermissions(newEidPermissions) { eidPermissions = newEidPermissions; diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 9d29b66cfc8..1dd1532f423 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -17,13 +17,14 @@ import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'; import {setImpBidParams} from '../../libraries/pbsExtensions/processors/params.js'; import {SUPPORTED_MEDIA_TYPES} from '../../libraries/pbsExtensions/processors/mediaType.js'; import {IMP, REQUEST, RESPONSE} from '../../src/pbjsORTB.js'; -import {beConvertCurrency} from '../../src/utils/currency.js'; import {redactor} from '../../src/activities/redactor.js'; import {s2sActivityParams} from '../../src/adapterManager.js'; import {activityParams} from '../../src/activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js'; +import {currencyCompare} from '../../libraries/currencyUtils/currency.js'; +import {minimum} from '../../src/utils/reducers.js'; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; @@ -117,6 +118,7 @@ const PBS_CONVERTER = ortbConverter({ src: CONSTANTS.S2S.SRC, bidId: bidRequest ? (bidRequest.bidId || bidRequest.bid_Id) : null, transactionId: context.adUnit.transactionId, + adUnitId: context.adUnit.adUnitId, auctionId: context.bidderRequest.auctionId, }), bidResponse), adUnit: context.adUnit.code @@ -141,6 +143,7 @@ const PBS_CONVERTER = ortbConverter({ bidfloor(orig, imp, proxyBidRequest, context) { // for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing, // and aggregate all of them into a single, minimum floor to put in the request + const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); let min; for (const req of context.actualBidRequests.values()) { const floor = {}; @@ -149,14 +152,8 @@ const PBS_CONVERTER = ortbConverter({ if (floor.bidfloorcur == null || floor.bidfloor == null) { min = null; break; - } else if (min == null) { - min = floor; - } else { - const value = beConvertCurrency(floor.bidfloor, floor.bidfloorcur, min.bidfloorcur); - if (value != null && value < min.bidfloor) { - min = floor; - } } + min = min == null ? floor : getMin(min, floor); } if (min != null) { Object.assign(imp, min); @@ -168,6 +165,10 @@ const PBS_CONVERTER = ortbConverter({ // FPD is handled different for PBS - the base request will only contain global FPD; // bidder-specific values are set in ext.prebid.bidderconfig + if (context.transmitTids) { + deepSetValue(ortbRequest, 'source.tid', proxyBidderRequest.auctionId); + } + mergeDeep(ortbRequest, context.s2sBidRequest.ortb2Fragments?.global); // also merge in s2sConfig.extPrebid @@ -240,7 +241,16 @@ const PBS_CONVERTER = ortbConverter({ }, fledgeAuctionConfigs(orig, response, ortbResponse, context) { const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({adUnitCode: impCtx.adUnit.code, config: cfg.config}))); + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => { + const bidderReq = impCtx.actualBidderRequests.find(br => br.bidderCode === cfg.bidder); + const bidReq = impCtx.actualBidRequests.get(cfg.bidder); + return { + adUnitCode: impCtx.adUnit.code, + ortb2: bidderReq?.ortb2, + ortb2Imp: bidReq?.ortb2Imp, + config: cfg.config + }; + })); if (configs.length > 0) { response.fledgeAuctionConfigs = configs; } diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js new file mode 100644 index 00000000000..9125f6f3911 --- /dev/null +++ b/modules/precisoBidAdapter.js @@ -0,0 +1,212 @@ +import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +const BIDDER_CODE = 'preciso'; +const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; +const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; +const GVLID = 874; +let userId = 'NA'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + gvlid: GVLID, + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && bid.params.host == 'prebid'); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + // userId = validBidRequests[0].userId.pubcid; + let winTop = window; + let location; + var offset = new Date().getTimezoneOffset(); + logInfo('timezone ' + offset); + var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + logInfo('location test' + city) + + const countryCode = getCountryCodeByTimezone(city); + logInfo(`The country code for ${city} is ${countryCode}`); + + // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin + try { + location = new URL(bidderRequest.refererInfo.page) + winTop = window.top; + } catch (e) { + location = winTop.location; + logMessage(e); + }; + + let request = { + id: validBidRequests[0].bidderRequestId, + + imp: validBidRequests.map(request => { + const { bidId, sizes, mediaType, ortb2 } = request + const item = { + id: bidId, + region: request.params.region, + traffic: mediaType, + bidFloor: getBidFloor(request), + ortb2: ortb2 + + } + + if (request.mediaTypes.banner) { + item.banner = { + format: (request.mediaTypes.banner.sizes || sizes).map(size => { + return { w: size[0], h: size[1] } + }), + } + } + + if (request.schain) { + item.schain = request.schain; + } + + if (request.floorData) { + item.bidFloor = request.floorData.floorMin; + } + return item + }), + auctionId: validBidRequests[0].auctionId, + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + geo: navigator.geolocation.getCurrentPosition(position => { + const { latitude, longitude } = position.coords; + return { + latitude: latitude, + longitude: longitude + } + // Show a map centered at latitude / longitude. + }) || { utcoffset: new Date().getTimezoneOffset() }, + city: city, + 'host': location.host, + 'page': location.pathname, + 'coppa': config.getConfig('coppa') === true ? 1 : 0 + // userId: validBidRequests[0].userId + }; + + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent; + } + } + + return { + method: 'POST', + url: AD_URL, + data: request, + + }; + }, + + interpretResponse: function (serverResponse) { + const response = serverResponse.body + + const bids = [] + + response.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + bids.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + ad: bid.adm, + currency: 'USD', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: bid.adomain || [], + }, + }) + }) + }) + + return bids + }, + + getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { + let syncs = []; + let { gdprApplies, consentString = '' } = gdprConsent; + + if (serverResponses.length > 0) { + logInfo('preciso bidadapter getusersync serverResponses:' + serverResponses.toString); + } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `${URL_SYNC}id=${userId}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=4` + }); + } else { + syncs.push({ + type: 'image', + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=2` + }); + } + + return syncs + } + +}; + +function getCountryCodeByTimezone(city) { + try { + const now = new Date(); + const options = { + timeZone: city, + timeZoneName: 'long', + }; + const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) + .formatToParts(now) + .filter((part) => part.type === 'timeZoneName'); + + if (timeZoneName) { + // Extract the country code from the timezone name + const parts = timeZoneName.value.split('-'); + if (parts.length >= 2) { + return parts[1]; + } + } + } catch (error) { + // Handle errors, such as an invalid timezone city + logInfo(error); + } + + // Handle the case where the city is not found or an error occurred + return 'Unknown'; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +registerBidder(spec); diff --git a/modules/precisoBidAdapter.md b/modules/precisoBidAdapter.md new file mode 100644 index 00000000000..b1fb0d062da --- /dev/null +++ b/modules/precisoBidAdapter.md @@ -0,0 +1,84 @@ +# Overview + +``` +Module Name: Preciso Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech@preciso.net +``` + +# Description + +Module that connects to preciso' demand sources + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :------------------------ | :------------------- | +| `region` | required (for prebid.js) | region | "prebid-eu" | +| `publisherId` | required (for prebid-server) | partner ID | "1901" | +| `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | + +# Test Parameters +``` + var adUnits = [ + // Will return static native ad. Assets are stored through user UI for each placement separetly + { + code: 'placementId_0', + mediaTypes: { + native: {} + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'native' + } + } + ] + }, + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'video' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 37167fff691..70a0f9b9a14 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -4,7 +4,6 @@ import { deepClone, deepSetValue, generateUUID, - getGptSlotInfoForAdUnitCode, getParameterByName, isNumber, logError, @@ -13,7 +12,8 @@ import { mergeDeep, parseGPTSingleSizeArray, parseUrl, - pick + pick, + deepEqual } from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {config} from '../src/config.js'; @@ -27,8 +27,9 @@ import {bidderSettings} from '../src/bidderSettings.js'; import {auctionManager} from '../src/auctionManager.js'; import {IMP, PBS, registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js'; -import {beConvertCurrency} from '../src/utils/currency.js'; import {adjustCpm} from '../src/utils/cpm.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertCurrency} from '../libraries/currencyUtils/currency.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -40,19 +41,22 @@ const MODULE_NAME = 'Price Floors'; */ const ajax = ajaxBuilder(10000); +// eslint-disable-next-line symbol-description +const SYN_FIELD = Symbol(); + /** * @summary Allowed fields for rules to have */ -export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; +export let allowedFields = [SYN_FIELD, 'gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; /** * @summary This is a flag to indicate if a AJAX call is processing for a floors request -*/ + */ let fetching = false; /** * @summary so we only register for our hooks once -*/ + */ let addedFloorsHook = false; /** @@ -90,8 +94,8 @@ const getHostname = (() => { })(); // First look into bidRequest! -function getGptSlotFromAdUnit(transactionId, {index = auctionManager.index} = {}) { - const adUnit = index.getAdUnit({transactionId}); +function getGptSlotFromAdUnit(adUnitId, {index = auctionManager.index} = {}) { + const adUnit = index.getAdUnit({adUnitId}); const isGam = deepAccess(adUnit, 'ortb2Imp.ext.data.adserver.name') === 'gam'; return isGam && adUnit.ortb2Imp.ext.data.adserver.adslot; } @@ -104,9 +108,10 @@ function getAdUnitCode(request, response, {index = auctionManager.index} = {}) { * @summary floor field types with their matching functions to resolve the actual matched value */ export let fieldMatchingFunctions = { + [SYN_FIELD]: () => '*', 'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*', 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', - 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, + 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).adUnitId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, 'domain': getHostname, 'adUnitCode': (bidRequest, bidResponse) => getAdUnitCode(bidRequest, bidResponse) } @@ -117,6 +122,7 @@ export let fieldMatchingFunctions = { * Returns array of Tuple [exact match, catch all] for each field in rules file */ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { + if (!floorFields.length) return []; // generate combination of all exact matches and catch all for each field type return floorFields.reduce((accum, field) => { let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*'; @@ -132,7 +138,9 @@ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { */ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) { let fieldValues = enumeratePossibleFieldValues(deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); - if (!fieldValues.length) return { matchingFloor: floorData.default }; + if (!fieldValues.length) { + return {matchingFloor: undefined} + } // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); @@ -146,9 +154,9 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], + floorRuleValue: floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters - matchingRule + matchingRule: matchingRule === floorData.meta?.defaultRule ? undefined : matchingRule }; // use adUnit floorMin as priority! const floorMin = deepAccess(bidObject, 'ortb2Imp.ext.prebid.floors.floorMin'); @@ -300,14 +308,20 @@ function normalizeRulesForAuction(floorData, adUnitCode) { * Only called if no set config or fetch level data has returned */ export function getFloorDataFromAdUnits(adUnits) { + const schemaAu = adUnits.find(au => au.floors?.schema != null); return adUnits.reduce((accum, adUnit) => { - if (isFloorsDataValid(adUnit.floors)) { + if (adUnit.floors?.schema != null && !deepEqual(adUnit.floors.schema, schemaAu?.floors?.schema)) { + logError(`${MODULE_NAME}: adUnit '${adUnit.code}' declares a different schema from one previously declared by adUnit '${schemaAu.code}'. Floor config for '${adUnit.code}' will be ignored.`) + return accum; + } + const floors = Object.assign({}, schemaAu?.floors, {values: undefined}, adUnit.floors) + if (isFloorsDataValid(floors)) { // if values already exist we want to not overwrite them if (!accum.values) { - accum = getFloorsDataForAuction(adUnit.floors, adUnit.code); + accum = getFloorsDataForAuction(floors, adUnit.code); accum.location = 'adUnit'; } else { - let newRules = getFloorsDataForAuction(adUnit.floors, adUnit.code).values; + let newRules = getFloorsDataForAuction(floors, adUnit.code).values; // copy over the new rules into our values object Object.assign(accum.values, newRules); } @@ -318,13 +332,29 @@ export function getFloorDataFromAdUnits(adUnits) { }, {}); } +function getNoFloorSignalBidersArray(floorData) { + const { data, enforcement } = floorData + // The data.noFloorSignalBidders higher priority then the enforcment + if (data?.noFloorSignalBidders?.length > 0) { + return data.noFloorSignalBidders + } else if (enforcement?.noFloorSignalBidders?.length > 0) { + return enforcement.noFloorSignalBidders + } + return [] +} + /** * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction */ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { + const noFloorSignalBiddersArray = getNoFloorSignalBidersArray(floorData) + adUnits.forEach((adUnit) => { adUnit.bids.forEach(bid => { - if (floorData.skipped) { + // check if the bidder is in the no signal list + const isNoFloorSignaled = noFloorSignalBiddersArray.some(bidderName => bidderName === bid.bidder) + if (floorData.skipped || isNoFloorSignaled) { + isNoFloorSignaled && logInfo(`noFloorSignal to ${bid.bidder}`) delete bid.getFloor; } else { bid.getFloor = getFloor; @@ -332,8 +362,10 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { // information for bid and analytics adapters bid.auctionId = auctionId; bid.floorData = { + noFloorSignaled: isNoFloorSignaled, skipped: floorData.skipped, - skipRate: floorData.skipRate, + skipRate: deepAccess(floorData, 'data.skipRate') ?? floorData.skipRate, + skippedReason: floorData.skippedReason, floorMin: floorData.floorMin, modelVersion: deepAccess(floorData, 'data.modelVersion'), modelWeight: deepAccess(floorData, 'data.modelWeight'), @@ -380,11 +412,13 @@ export function createFloorsDataForAuction(adUnits, auctionId) { // if we still do not have a valid floor data then floors is not on for this auction, so skip if (Object.keys(deepAccess(resolvedFloorsData, 'data.values') || {}).length === 0) { resolvedFloorsData.skipped = true; + resolvedFloorsData.skippedReason = CONSTANTS.FLOOR_SKIPPED_REASON.NOT_FOUND } else { // determine the skip rate now - const auctionSkipRate = getParameterByName('pbjs_skipRate') || resolvedFloorsData.skipRate; + const auctionSkipRate = getParameterByName('pbjs_skipRate') || (deepAccess(resolvedFloorsData, 'data.skipRate') ?? resolvedFloorsData.skipRate); const isSkipped = Math.random() * 100 < parseFloat(auctionSkipRate); resolvedFloorsData.skipped = isSkipped; + if (isSkipped) resolvedFloorsData.skippedReason = CONSTANTS.FLOOR_SKIPPED_REASON.RANDOM } // copy FloorMin to floorData.data if (resolvedFloorsData.hasOwnProperty('floorMin')) resolvedFloorsData.data.floorMin = resolvedFloorsData.floorMin; @@ -414,10 +448,13 @@ export function continueAuction(hookConfig) { } function validateSchemaFields(fields) { - if (Array.isArray(fields) && fields.length > 0 && fields.every(field => allowedFields.indexOf(field) !== -1)) { - return true; + if (Array.isArray(fields) && fields.length > 0) { + if (fields.every(field => allowedFields.includes(field))) { + return true; + } else { + logError(`${MODULE_NAME}: Fields received do not match allowed fields`); + } } - logError(`${MODULE_NAME}: Fields recieved do not match allowed fields`); return false; } @@ -443,7 +480,26 @@ function validateRules(floorsData, numFields, delimiter) { return Object.keys(floorsData.values).length > 0; } +export function normalizeDefault(model) { + if (isNumber(model.default)) { + let defaultRule = '*'; + const numFields = (model.schema?.fields || []).length; + if (!numFields) { + deepSetValue(model, 'schema.fields', [SYN_FIELD]); + } else { + defaultRule = Array(numFields).fill('*').join(model.schema?.delimiter || '|'); + } + model.values = model.values || {}; + if (model.values[defaultRule] == null) { + model.values[defaultRule] = model.default; + model.meta = {defaultRule}; + } + } + return model; +} + function modelIsValid(model) { + model = normalizeDefault(model); // schema.fields has only allowed attributes if (!validateSchemaFields(deepAccess(model, 'schema.fields'))) { return false; @@ -583,7 +639,7 @@ function handleFetchError(status) { } /** - * This function handles sending and recieving the AJAX call for a floors fetch + * This function handles sending and receiving the AJAX call for a floors fetch * @param {object} floorsConfig the floors config coming from setConfig */ export function generateAndHandleFetch(floorEndpoint) { @@ -630,7 +686,8 @@ export function handleSetFloorsConfig(config) { 'enforceJS', enforceJS => enforceJS !== false, // defaults to true 'enforcePBS', enforcePBS => enforcePBS === true, // defaults to false 'floorDeals', floorDeals => floorDeals === true, // defaults to false - 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true + 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true, + 'noFloorSignalBidders', noFloorSignalBidders => noFloorSignalBidders || [] ]), 'additionalSchemaFields', additionalSchemaFields => typeof additionalSchemaFields === 'object' && Object.keys(additionalSchemaFields).length > 0 ? addFieldOverrides(additionalSchemaFields) : undefined, 'data', data => (data && parseFloorData(data, 'setConfig')) || undefined @@ -715,7 +772,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a let floorInfo = getFirstMatchingFloor(floorData.data, matchingBidRequest, {...bid, size: [bid.width, bid.height]}); if (!floorInfo.matchingFloor) { - logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); + if (floorInfo.matchingFloor !== 0) logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); return fn.call(this, adUnitCode, bid, reject); } @@ -794,8 +851,8 @@ export function setImpExtPrebidFloors(imp, bidRequest, context) { if (floorMinCur == null) { floorMinCur = imp.bidfloorcur } const ortb2ImpFloorCur = imp.ext?.prebid?.floors?.floorMinCur || imp.ext?.prebid?.floorMinCur || floorMinCur; const ortb2ImpFloorMin = imp.ext?.prebid?.floors?.floorMin || imp.ext?.prebid?.floorMin; - const convertedFloorMinValue = beConvertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); - const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? beConvertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; + const convertedFloorMinValue = convertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); + const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? convertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; const lowestImpFloorMin = convertedOrtb2ImpFloorMinValue && convertedOrtb2ImpFloorMinValue < convertedFloorMinValue ? convertedOrtb2ImpFloorMinValue diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index 7c9108f60b1..b42c4b8af3f 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -2,7 +2,16 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'prisma'; const BIDDER_URL = 'https://prisma.nexx360.io/prebid'; @@ -44,20 +53,20 @@ export const spec = { aliases: ['prismadirect'], // short code supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { return !!(bid.params.account && bid.params.tagId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const adUnits = []; const test = config.getConfig('debug') ? 1 : 0; @@ -109,11 +118,11 @@ export const spec = { }; }, /** - * 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. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { const serverBody = serverResponse.body; const bidResponses = []; @@ -163,12 +172,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { @@ -179,9 +188,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction - */ + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid the bid that won the auction + */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid const params = { type: 'prebid', mediatype: 'banner' }; diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js new file mode 100644 index 00000000000..7d52e305189 --- /dev/null +++ b/modules/programmaticaBidAdapter.js @@ -0,0 +1,153 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; + +const BIDDER_CODE = 'programmatica'; +const DEFAULT_ENDPOINT = 'asr.programmatica.com'; +const SYNC_ENDPOINT = 'sync.programmatica.com'; +const ADOMAIN = 'programmatica.com'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + let valid = bid.params.siteId && bid.params.placementId; + + return !!valid; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + for (const bid of validBidRequests) { + let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT; + + requests.push({ + method: 'GET', + url: `https://${endpoint}/get`, + data: { + site_id: bid.params.siteId, + placement_id: bid.params.placementId, + prebid: true, + }, + bidRequest: bid, + }); + } + + return requests; + }, + + interpretResponse: function(serverResponse, request) { + if (!serverResponse?.body?.content?.data) { + return []; + } + + const bidResponses = []; + const body = serverResponse.body; + + let mediaType = BANNER; + let ad, vastXml; + let width; + let height; + + let sizes = getSize(body.size); + if (isArray(sizes)) { + [width, height] = sizes; + } + + if (body.type.format != '') { + // banner + ad = body.content.data; + if (body.content.imps?.length) { + for (const imp of body.content.imps) { + ad += ``; + } + } + } else { + // video + vastXml = body.content.data; + mediaType = VIDEO; + + if (!width || !height) { + const pSize = deepAccess(request.bidRequest, 'mediaTypes.video.playerSize'); + const reqSize = getSize(pSize); + if (isArray(reqSize)) { + [width, height] = reqSize; + } + } + } + + const bidResponse = { + requestId: request.bidRequest.bidId, + cpm: body.cpm, + currency: body.currency || 'USD', + width: parseInt(width), + height: parseInt(height), + creativeId: body.id, + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: ad, + mediaType: mediaType, + vastXml: vastXml, + meta: { + advertiserDomains: [ADOMAIN], + } + }; + + if ((mediaType === VIDEO && request.bidRequest.mediaTypes?.video) || (mediaType === BANNER && request.bidRequest.mediaTypes?.banner)) { + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + if (!hasPurpose1Consent(gdprConsent)) { + return syncs; + } + + let params = `usp=${uspConsent ?? ''}&consent=${gdprConsent?.consentString ?? ''}`; + if (typeof gdprConsent?.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}` + }); + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${SYNC_ENDPOINT}/match/sp?${params}` + }); + } + + return syncs; + }, + + onTimeout: function(timeoutData) {}, + onBidWon: function(bid) {}, + onSetTargeting: function(bid) {}, + onBidderError: function() {}, + supportedMediaTypes: [ BANNER, VIDEO ] +} + +registerBidder(spec); + +function getSize(paramSizes) { + const parsedSizes = parseSizesInput(paramSizes); + const sizes = parsedSizes.map(size => { + const [width, height] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return [w, h]; + }); + + return sizes[0] || null; +} diff --git a/modules/programmaticaBidAdapter.md b/modules/programmaticaBidAdapter.md new file mode 100644 index 00000000000..5982edf143e --- /dev/null +++ b/modules/programmaticaBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: Programmatica Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@programmatica.com +``` + +# Description +Connects to Programmatica server for bids. +Module supports banner and video mediaType. + +# Test Parameters + +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cgim20sipgj0vj1cb510' + } + }] + }, + { + code: '/test/div', + mediaTypes: { + video: { + playerSize: [[640, 360]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cioghpcipgj8r721e9ag' + } + }] + },]; +``` diff --git a/modules/pstudioBidAdapter.js b/modules/pstudioBidAdapter.js new file mode 100644 index 00000000000..77a11ac58c6 --- /dev/null +++ b/modules/pstudioBidAdapter.js @@ -0,0 +1,435 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { + deepAccess, + isArray, + isNumber, + generateUUID, + isEmpty, + isFn, + isPlainObject, +} from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const BIDDER_CODE = 'pstudio'; +const ENDPOINT = 'https://exchange.pstudio.tadex.id/prebid-bid' +const TIME_TO_LIVE = 300; +// in case that the publisher limits number of user syncs, thisse syncs will be discarded from the end of the list +// so more improtant syncing calls should be at the start of the list +const USER_SYNCS = [ + // PARTNER_UID is a partner user id + { + type: 'img', + url: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=k1on5ig&ttd_tpi=1&ttd_puid=%PARTNER_UID%&dsp=ttd', + macro: '%PARTNER_UID%', + }, + { + type: 'img', + url: 'https://dsp.myads.telkomsel.com/api/v1/pixel?uid=%USERID%', + macro: '%USERID%', + }, +]; +const COOKIE_NAME = '__tadexid'; +const COOKIE_TTL_DAYS = 365; +const DAY_IN_MS = 24 * 60 * 60 * 1000; +const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; +const VIDEO_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'protocols', + 'startdelay', + 'placement', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity', +]; + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + + isBidRequestValid: function (bid) { + const params = bid.params || {}; + return !!params.pubid && !!params.floorPrice && isVideoRequestValid(bid); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map((bid) => ({ + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(buildRequestData(bid, bidderRequest)), + options: { + contentType: 'application/json', + withCredentials: true, + }, + })); + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + + if (!serverResponse.body.bids) return []; + const { id } = JSON.parse(bidRequest.data); + + serverResponse.body.bids.map((bid) => { + const { cpm, width, height, currency, ad, meta } = bid; + let bidResponse = { + requestId: id, + cpm, + width, + height, + creativeId: bid.creative_id, + currency, + netRevenue: bid.net_revenue, + ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: meta.advertiser_domains, + }, + }; + + if (bid.vast_url || bid.vast_xml) { + bidResponse.vastUrl = bid.vast_url; + bidResponse.vastXml = bid.vast_xml; + bidResponse.mediaType = VIDEO; + } else { + bidResponse.ad = ad; + } + + bidResponses.push(bidResponse); + }); + + return bidResponses; + }, + + getUserSyncs(_optionsType, _serverResponse, _gdprConsent, _uspConsent) { + const syncs = []; + + let userId = readUserIdFromCookie(COOKIE_NAME); + + if (!userId) { + userId = generateId(); + writeIdToCookie(COOKIE_NAME, userId); + } + + USER_SYNCS.map((userSync) => { + if (userSync.type === 'img') { + syncs.push({ + type: 'image', + url: userSync.url.replace(userSync.macro, userId), + }); + } + }); + + return syncs; + }, +}; + +function buildRequestData(bid, bidderRequest) { + let payloadObject = buildBaseObject(bid, bidderRequest); + + if (bid.mediaTypes.banner) { + return buildBannerObject(bid, payloadObject); + } else if (bid.mediaTypes.video) { + return buildVideoObject(bid, payloadObject); + } +} + +function buildBaseObject(bid, bidderRequest) { + const firstPartyData = prepareFirstPartyData(bidderRequest.ortb2); + const { pubid, bcat, badv, bapp } = bid.params; + const { userId } = bid; + const uid2Token = userId?.uid2?.id; + + if (uid2Token) { + if (firstPartyData.user) { + firstPartyData.user.uid2_token = uid2Token; + } else { + firstPartyData.user = { uid2_token: uid2Token }; + } + } + const userCookieId = readUserIdFromCookie(COOKIE_NAME); + if (userCookieId) { + if (firstPartyData.user) { + firstPartyData.user.id = userCookieId; + } else { + firstPartyData.user = { id: userCookieId }; + } + } + + return { + id: bid.bidId, + pubid, + floor_price: getBidFloor(bid), + adtagid: bid.adUnitCode, + ...(bcat && { bcat }), + ...(badv && { badv }), + ...(bapp && { bapp }), + ...firstPartyData, + }; +} + +function buildBannerObject(bid, payloadObject) { + const { sizes, pos, name } = bid.mediaTypes.banner; + + payloadObject.banner_properties = { + name, + sizes, + pos, + }; + + return payloadObject; +} + +function buildVideoObject(bid, payloadObject) { + const { context, playerSize, w, h } = bid.mediaTypes.video; + + payloadObject.video_properties = { + context, + w: w || playerSize[0][0], + h: h || playerSize[0][1], + }; + + for (const param of VIDEO_PARAMS) { + const paramValue = deepAccess(bid, `mediaTypes.video.${param}`); + + if (paramValue) { + payloadObject.video_properties[param] = paramValue; + } + } + + return payloadObject; +} + +function readUserIdFromCookie(key) { + try { + const storedValue = storage.getCookie(key); + + if (storedValue !== null) { + return storedValue; + } + } catch (error) { + } +} + +function generateId() { + return generateUUID(); +} + +function daysToMs(days) { + return days * DAY_IN_MS; +} + +function writeIdToCookie(key, value) { + if (storage.cookiesAreEnabled()) { + const expires = new Date( + Date.now() + daysToMs(parseInt(COOKIE_TTL_DAYS)) + ).toUTCString(); + storage.setCookie(key, value, expires, '/'); + } +} + +function prepareFirstPartyData({ user, device, site, app, regs }) { + let userData; + let deviceData; + let siteData; + let appData; + let regsData; + + if (user) { + userData = { + yob: user.yob, + gender: user.gender, + }; + } + + if (device) { + deviceData = { + ua: device.ua, + dnt: device.dnt, + lmt: device.lmt, + ip: device.ip, + ipv6: device.ipv6, + devicetype: device.devicetype, + make: device.make, + model: device.model, + os: device.os, + osv: device.osv, + js: device.js, + language: device.language, + carrier: device.carrier, + connectiontype: device.connectiontype, + ifa: device.ifa, + ...(device.geo && { + geo: { + lat: device.geo.lat, + lon: device.geo.lon, + country: device.geo.country, + region: device.geo.region, + regionfips104: device.geo.regionfips104, + metro: device.geo.metro, + city: device.geo.city, + zip: device.geo.zip, + type: device.geo.type, + }, + }), + ...(device.ext && { + ext: { + ifatype: device.ext.ifatype, + }, + }), + }; + } + + if (site) { + siteData = { + id: site.id, + name: site.name, + domain: site.domain, + page: site.page, + cat: site.cat, + sectioncat: site.sectioncat, + pagecat: site.pagecat, + ref: site.ref, + ...(site.publisher && { + publisher: { + name: site.publisher.name, + cat: site.publisher.cat, + domain: site.publisher.domain, + }, + }), + ...(site.content && { + content: { + id: site.content.id, + episode: site.content.episode, + title: site.content.title, + series: site.content.series, + artist: site.content.artist, + genre: site.content.genre, + album: site.content.album, + isrc: site.content.isrc, + season: site.content.season, + }, + }), + mobile: site.mobile, + }; + } + + if (app) { + appData = { + id: app.id, + name: app.name, + bundle: app.bundle, + domain: app.domain, + storeurl: app.storeurl, + cat: app.cat, + sectioncat: app.sectioncat, + pagecat: app.pagecat, + ver: app.ver, + privacypolicy: app.privacypolicy, + paid: app.paid, + ...(app.publisher && { + publisher: { + name: app.publisher.name, + cat: app.publisher.cat, + domain: app.publisher.domain, + }, + }), + keywords: app.keywords, + ...(app.content && { + content: { + id: app.content.id, + episode: app.content.episode, + title: app.content.title, + series: app.content.series, + artist: app.content.artist, + genre: app.content.genre, + album: app.content.album, + isrc: app.content.isrc, + season: app.content.season, + }, + }), + }; + } + + if (regs) { + regsData = { coppa: regs.coppa }; + } + + return cleanObject({ + user: userData, + device: deviceData, + site: siteData, + app: appData, + regs: regsData, + }); +} + +function cleanObject(data) { + for (let key in data) { + if (typeof data[key] == 'object') { + cleanObject(data[key]); + + if (isEmpty(data[key])) delete data[key]; + } + + if (data[key] === undefined) delete data[key]; + } + + return data; +} + +function isVideoRequestValid(bidRequest) { + if (bidRequest.mediaTypes.video) { + const { w, h, playerSize, mimes, protocols } = deepAccess( + bidRequest, + 'mediaTypes.video', + {} + ); + + const areSizesValid = + (isNumber(w) && isNumber(h)) || validateSizes(playerSize); + const areMimesValid = isArray(mimes) && mimes.length > 0; + const areProtocolsValid = + isArray(protocols) && protocols.length > 0 && protocols.every(isNumber); + + return areSizesValid && areMimesValid && areProtocolsValid; + } + + return true; +} + +function validateSizes(sizes) { + return ( + isArray(sizes) && + sizes.length > 0 && + sizes.every( + (size) => isArray(size) && size.length === 2 && size.every(isNumber) + ) + ); +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return bid.params.floorPrice ? bid.params.floorPrice : null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/pstudioBidAdapter.md b/modules/pstudioBidAdapter.md new file mode 100644 index 00000000000..0c8c6927f43 --- /dev/null +++ b/modules/pstudioBidAdapter.md @@ -0,0 +1,148 @@ +# Overview + +``` +Module Name: PStudio Bid Adapter +Module Type: Bidder Adapter +Maintainer: pstudio@telkomsel.com +``` + +# Description + +Currently module supports banner as well as instream video mediaTypes. + + +# Test parameters + +Those parameters should be used to get test responses from the adapter. + +```js +var adUnits = [ + // Banner ad unit + { + code: 'test-div-1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'pstudio', + params: { + // id of test publisher + pubid: '22430f9d-9610-432c-aabe-6134256f11af', + floorPrice: 1.25, + }, + }, + ], + }, + // Instream video ad unit + { + code: 'test-div-2', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [3], + }, + }, + bids: [ + { + bidder: 'pstudio', + params: { + // id of test publisher + pubid: '22430f9d-9610-432c-aabe-6134256f11af', + floorPrice: 1.25, + }, + }, + ], + }, +]; +``` + +# Sample Banner Ad Unit + +```js +var adUnits = [ + { + code: 'test-div-1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + pos: 0, + name: 'test-name', + }, + }, + bids: [ + { + bidder: 'pstudio', + params: { + pubid: '22430f9d-9610-432c-aabe-6134256f11af', // required + floorPrice: 1.15, // required + bcat: ['IAB1-1', 'IAB1-3'], // optional + badv: ['nike.com'], // optional + bapp: ['com.foo.mygame'], // optional + }, + }, + ], + }, +]; +``` + +# Sample Video Ad Unit + +```js +var videoAdUnits = [ + { + code: 'test-instream-video', + mediaTypes: { + video: { + context: 'instream', // required (only instream accepted) + playerSize: [640, 480], // required (alternatively it could be pair of `w` and `h` parameters) + mimes: ['video/mp4'], // required (only choices `video/mp4`, `application/javascript`, `video/webm` and `video/ogg` supported) + protocols: [2, 3], // 1 required (only choices 2 and 3 supported) + minduration: 5, // optional + maxduration: 30, // optional + startdelay: 5, // optional + placement: 1, // optional (only 1 accepted, as it is instream placement) + skip: 1, // optional + skipafter: 1, // optional + minbitrate: 10, // optional + maxbitrate: 10, // optional + delivery: 1, // optional + playbackmethod: [1, 3], // optional + api: [2], // optional (only choice 2 supported) + linearity: 1, // optional + }, + }, + bids: [ + { + bidder: 'pstudio', + params: { + pubid: '22430f9d-9610-432c-aabe-6134256f11af', + floorPrice: 1.25, + badv: ['adidas.com'], + }, + }, + ], + }, +]; +``` + +# Configuration for video + +### Prebid cache + +For video ads, Prebid cache must be enabled, as the demand partner does not support caching of video content. + +```js +pbjs.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache', + }, +}); +``` + +Please provide Prebid Cache of your choice. This example uses AppNexus cache, but if you use other cache, change it according to your needs. + diff --git a/modules/pubCircleBidAdapter.js b/modules/pubCircleBidAdapter.js new file mode 100644 index 00000000000..54224fd0403 --- /dev/null +++ b/modules/pubCircleBidAdapter.js @@ -0,0 +1,231 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pubcircle'; +const AD_URL = 'https://ml.pubcircle.ai/pbjs'; +const SYNC_URL = 'https://cs.pubcircle.ai'; + +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 || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + placement.placementId = placementId; + placement.type = 'publisher'; + + if (bid.userId) { + getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); + getUserId(placement.eids, bid.userId.idx, 'idx.lat'); + getUserId(placement.eids, bid.userId.idl_env, 'liveramp.com'); + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.placementId); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + 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)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidViewable: function (bid) { + // to do : we need to implement js tag to fire pixel with viewability counter + } +}; + +registerBidder(spec); diff --git a/modules/pubCircleBidAdapter.md b/modules/pubCircleBidAdapter.md new file mode 100644 index 00000000000..4fc114bf20c --- /dev/null +++ b/modules/pubCircleBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PubCirlce Bidder Adapter +Module Type: PubCirlce Bidder Adapter +Maintainer: system@smartyads.com +``` + +# Description + +Connects to PubCirlce exchange for bids. +PubCirlce bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'addunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/pubProvidedIdSystem.js b/modules/pubProvidedIdSystem.js index baffd997443..d23d992e495 100644 --- a/modules/pubProvidedIdSystem.js +++ b/modules/pubProvidedIdSystem.js @@ -9,6 +9,11 @@ import {submodule} from '../src/hook.js'; import { logInfo, isArray } from '../src/utils.js'; import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'pubProvidedId'; /** @type {Submodule} */ @@ -25,7 +30,7 @@ export const pubProvidedIdSubmodule = { * decode the stored id value for passing to bid request * @function * @param {string} value - * @returns {{pubProvidedId: array}} or undefined if value doesn't exists + * @returns {{pubProvidedId: Array}} or undefined if value doesn't exists */ decode(value) { const res = value ? {pubProvidedId: value} : undefined; @@ -37,7 +42,7 @@ export const pubProvidedIdSubmodule = { * performs action to obtain id and return a value. * @function * @param {SubmoduleConfig} [config] - * @returns {{id: array}} + * @returns {{id: Array}} */ getId(config) { const configParams = (config && config.params) || {}; diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 5b20dbb620a..e8eb90cd02a 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -12,10 +12,19 @@ import { parseUrl, buildUrl, logError } from '../src/utils.js'; import {uspDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'publinkId'; const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; +const PUBLINK_REQUEST_PATH = '/cvx/client/sync/publink'; +const PUBLINK_REFRESH_PATH = '/cvx/client/sync/publink/refresh'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -23,10 +32,9 @@ function isHex(s) { return /^[A-F0-9]+$/i.test(s); } -function publinkIdUrl(params, consentData) { - let url = parseUrl('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink'); +function publinkIdUrl(params, consentData, storedId) { + let url = parseUrl('https://proc.ad.cpe.dotomi.com' + PUBLINK_REFRESH_PATH); url.search = { - deh: params.e, mpn: 'Prebid.js', mpv: '$prebid.version$', }; @@ -36,9 +44,21 @@ function publinkIdUrl(params, consentData) { url.search.gdpr_consent = consentData.consentString; } - if (params.site_id) { url.search.sid = params.site_id; } + if (params) { + if (params.e) { + // if there's an email parameter call the request path + url.search.deh = params.e; + url.pathname = PUBLINK_REQUEST_PATH; + } + + if (params.site_id) { url.search.sid = params.site_id; } + + if (params.api_key) { url.search.apikey = params.api_key; } + } - if (params.api_key) { url.search.apikey = params.api_key; } + if (storedId) { + url.search.publink = storedId; + } const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString && typeof usPrivacyString === 'string') { @@ -48,7 +68,7 @@ function publinkIdUrl(params, consentData) { return buildUrl(url); } -function makeCallback(config = {}, consentData) { +function makeCallback(config = {}, consentData, storedId) { return function(prebidCallback) { const options = {method: 'GET', withCredentials: true}; let handleResponse = function(responseText, xhr) { @@ -59,15 +79,12 @@ function makeCallback(config = {}, consentData) { } } }; - - if (config.params && config.params.e) { - if (isHex(config.params.e)) { - ajax(publinkIdUrl(config.params, consentData), handleResponse, undefined, options); - } else { - logError('params.e must be a hex string'); - } + if ((config.params && config.params.e && isHex(config.params.e)) || storedId) { + ajax(publinkIdUrl(config.params, consentData, storedId), handleResponse, undefined, options); + } else if (config.params.e) { + logError('params.e must be a hex string'); } - }; + } } function getlocalValue() { @@ -137,9 +154,7 @@ export const publinkIdSubmodule = { if (localValue) { return {id: localValue}; } - if (!storedId) { - return {callback: makeCallback(config, consentData)}; - } + return {callback: makeCallback(config, consentData, storedId)}; }, eids: { 'publinkId': { diff --git a/modules/publirBidAdapter.js b/modules/publirBidAdapter.js new file mode 100644 index 00000000000..432e123458c --- /dev/null +++ b/modules/publirBidAdapter.js @@ -0,0 +1,391 @@ +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + getDNT, + getBidIdParameter +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { ajax } from '../src/ajax.js'; + +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +const BIDDER_CODE = 'publir'; +const ADAPTER_VERSION = '1.0.0'; +const TTL = 360; +const CURRENCY = 'USD'; +const DEFAULT_SELLER_ENDPOINT = 'https://prebid.publir.com/publirPrebidEndPoint'; +const DEFAULT_IMPS_ENDPOINT = 'https://prebidimpst.publir.com/publirPrebidImpressionTracker'; +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); +export const spec = { + code: BIDDER_CODE, + version: ADAPTER_VERSION, + aliases: ['plr'], + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function (bidRequest) { + if (!bidRequest.params.pubId) { + logWarn('pubId is a mandatory param for Publir adapter'); + return false; + } + + return true; + }, + buildRequests: function (validBidRequests, bidderRequest) { + const reqObj = {}; + const generalObject = validBidRequests[0]; + reqObj.params = generatePubGeneralsParams(generalObject, bidderRequest); + reqObj.bids = generatePubBidParams(validBidRequests, bidderRequest); + reqObj.bids.timestamp = timestamp(); + let options = { + withCredentials: false + }; + + return { + method: 'POST', + url: DEFAULT_SELLER_ENDPOINT, + data: reqObj, + options + } + }, + interpretResponse: function ({ body }) { + const bidResponses = []; + if (body.bids) { + body.bids.forEach(adUnit => { + const bidResponse = { + requestId: adUnit.requestId, + cpm: adUnit.cpm, + currency: adUnit.currency || CURRENCY, + width: adUnit.width, + height: adUnit.height, + ttl: adUnit.ttl || TTL, + creativeId: adUnit.creativeId, + netRevenue: adUnit.netRevenue || true, + nurl: adUnit.nurl, + mediaType: adUnit.mediaType, + meta: { + mediaType: adUnit.mediaType + }, + }; + + if (adUnit.mediaType === VIDEO) { + bidResponse.vastXml = adUnit.vastXml; + } else if (adUnit.mediaType === BANNER) { + bidResponse.ad = adUnit.ad; + } + + if (adUnit.adomain && adUnit.adomain.length) { + bidResponse.meta.advertiserDomains = adUnit.adomain; + } else { + bidResponse.meta.advertiserDomains = []; + } + if (adUnit?.meta?.ad_key) { + bidResponse.meta.ad_key = adUnit.meta.ad_key ?? null; + } + if (adUnit.campId) { + bidResponse.campId = adUnit.campId; + } + bidResponse.bidder = BIDDER_CODE; + bidResponses.push(bidResponse); + }); + } else { + return []; + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (response.body && response.body.params) { + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.params.userSyncURL + }); + } + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + } + return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } + logInfo('onBidWon:', bid); + ajax(DEFAULT_IMPS_ENDPOINT, null, JSON.stringify(bid), { method: 'POST', mode: 'no-cors', credentials: 'include', headers: { 'Content-Type': 'application/json' } }); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } + }, +}; + +registerBidder(spec); + +/** + * Get floor price + * @param bid {bid} + * @returns {Number} + */ +function getFloor(bid, mediaType) { + if (!isFn(bid.getFloor)) { + return 0; + } + let floorResult = bid.getFloor({ + currency: CURRENCY, + mediaType: mediaType, + size: '*' + }); + return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; +} + +/** + * Get the the ad sizes array from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizesArray(bid, mediaType) { + let sizesArray = [] + + if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { + sizesArray = bid.mediaTypes[mediaType].sizes; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizesArray = bid.sizes; + } + + return sizesArray; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; + }); + return scStr; +} + +/** + * Get encoded node value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !isEmpty(val) ? encodeURIComponent(val) : ''; +} + +function getAllowedSyncMethod(filterSettings, bidderCode) { + const iframeConfigsToCheck = ['all', 'iframe']; + const pixelConfigToCheck = 'image'; + if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; + } + if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; + } +} + +/** + * Check if sync rule is supported + * @param syncRule {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(syncRule, bidderCode) { + if (!syncRule) { + return false; + } + const isInclude = syncRule.filter === 'include'; + const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && contains(bidders, bidderCode); +} + +/** + * get device type + * @param ua {ua} + * @returns {string} + */ +function getDeviceType(ua) { + if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(ua.toLowerCase())) { + return '5'; + } + if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(ua.toLowerCase())) { + return '4'; + } + if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i.test(ua.toLowerCase())) { + return '3'; + } + return '1'; +} + +function generatePubBidParams(validBidRequests, bidderRequest) { + const bidsArray = []; + + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); + } + return bidsArray; +} + +/** + * Generate bid specific parameters + * @param {bid} bid + * @param {bidderRequest} bidderRequest + * @returns {Object} bid specific params object + */ +function generateBidParameters(bid, bidderRequest) { + const { params } = bid; + const mediaType = isBanner(bid) ? BANNER : VIDEO; + const sizesArray = getSizesArray(bid, mediaType); + + // fix floor price in case of NAN + if (isNaN(params.floorPrice)) { + params.floorPrice = 0; + } + + const bidObject = { + mediaType, + adUnitCode: getBidIdParameter('adUnitCode', bid), + sizes: sizesArray, + floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + bidId: getBidIdParameter('bidId', bid), + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + loop: getBidIdParameter('bidderRequestsCount', bid), + transactionId: bid.ortb2Imp?.ext?.tid, + coppa: 0 + }; + + const pubId = params.pubId; + if (pubId) { + bidObject.pubId = pubId; + } + + const sua = deepAccess(bid, `ortb2.device.sua`); + if (sua) { + bidObject.sua = sua; + } + + const coppa = deepAccess(bid, `ortb2.regs.coppa`) + if (coppa) { + bidObject.coppa = 1; + } + + return bidObject; +} + +function isBanner(bid) { + return bid.mediaTypes && bid.mediaTypes.banner; +} + +function getLocalStorage(cookieObjName) { + if (storage.localStorageIsEnabled()) { + const lstData = storage.getDataFromLocalStorage(cookieObjName); + return lstData; + } + return ''; +} + +/** + * Generate params that are common between all bids + * @param generalObject + * @param {bidderRequest} bidderRequest + * @returns {object} the common params object + */ +function generatePubGeneralsParams(generalObject, bidderRequest) { + const domain = bidderRequest.refererInfo; + const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; + const { bidderCode } = bidderRequest; + const generalBidParams = generalObject.params; + const timeout = bidderRequest.timeout; + const generalParams = { + wrapper_type: 'prebidjs', + wrapper_vendor: '$$PREBID_GLOBAL$$', + wrapper_version: '$prebid.version$', + adapter_version: ADAPTER_VERSION, + auction_start: bidderRequest.auctionStart, + publisher_id: generalBidParams.pubId, + publisher_name: domain, + site_domain: domain, + dnt: getDNT() ? 1 : 0, + device_type: getDeviceType(navigator.userAgent), + ua: navigator.userAgent, + is_wrapper: !!generalBidParams.isWrapper, + session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), + tmax: timeout, + user_cookie: getLocalStorage('_publir_prebid_creative') + }; + + const userIdsParam = getBidIdParameter('userId', generalObject); + if (userIdsParam) { + generalParams.userIds = JSON.stringify(userIdsParam); + } + + const ortb2Metadata = bidderRequest.ortb2 || {}; + if (ortb2Metadata.site) { + generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); + } + if (ortb2Metadata.user) { + generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + generalParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + generalParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (generalObject.schain) { + generalParams.schain = getSupplyChain(generalObject.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); + } + + return generalParams; +} diff --git a/modules/publirBidAdapter.md b/modules/publirBidAdapter.md new file mode 100644 index 00000000000..872fd40c2ae --- /dev/null +++ b/modules/publirBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +``` +Module Name: Publir Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@publir.com +``` + + +# Description + +Module that connects to Publir's demand sources. + +The Publir adapter requires setup and approval from the Publir. Please reach out to info@publir.com to create an Publir account. + +The adapter supports Video(instream). + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `pubId` | required | String | Publir publisher Id provided by your Publir representative | "1234567890abcdef12345678" + + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'hre_div-hre-vcn-1', + sizes: [[1080, 1920]]], + mediaTypes: { + banner: { + sizes: [ + [1080, 1920], + ], + }, + }, + bids: [{ + bidder: 'publir', + params: { + pubId: '1234567890abcdef12345678' + } + }] + } + ]; +``` diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index acae93c57be..ced47086f7b 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -1,13 +1,15 @@ -import { _each, pick, logWarn, isStr, isArray, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; +import {_each, isArray, isStr, logError, logWarn, pick, generateUUID} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /// /////////// CONSTANTS ////////////// const ADAPTER_CODE = 'pubmatic'; +const VENDOR_OPENWRAP = 'openwrap'; const SEND_TIMEOUT = 2000; const END_POINT_HOST = 'https://t.pubmatic.com/'; const END_POINT_BID_LOGGER = END_POINT_HOST + 'wl?'; @@ -22,6 +24,7 @@ const ERROR = 'error'; const REQUEST_ERROR = 'request-error'; const TIMEOUT_ERROR = 'timeout-error'; const EMPTY_STRING = ''; +const OPEN_AUCTION_DEAL_ID = '-1'; const MEDIA_TYPE_BANNER = 'banner'; const CURRENCY_USD = 'USD'; const BID_PRECISION = 2; @@ -91,7 +94,7 @@ function copyRequiredBidDetails(bid) { 'bidderCode', 'adapterCode', 'bidId', - 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out + 'status', () => NO_BID, // default a bid to NO_BID until response is received or bid is timed out 'finalSource as source', 'params', 'floorData', @@ -148,6 +151,7 @@ function parseBidResponse(bid) { 'cpm', () => window.parseFloat(Number(bid.cpm).toFixed(BID_PRECISION)), 'originalCpm', () => window.parseFloat(Number(bid.originalCpm).toFixed(BID_PRECISION)), 'originalCurrency', + 'adserverTargeting', 'dealChannel', 'meta', 'status', @@ -256,12 +260,28 @@ function isS2SBidder(bidder) { return (s2sBidders.indexOf(bidder) > -1) ? 1 : 0 } +function isOWPubmaticBid(adapterName) { + let s2sConf = config.getConfig('s2sConfig'); + let s2sConfArray = isArray(s2sConf) ? s2sConf : [s2sConf]; + return s2sConfArray.some(conf => { + if (adapterName === ADAPTER_CODE && conf.defaultVendor === VENDOR_OPENWRAP && + conf.bidders.indexOf(ADAPTER_CODE) > -1) { + return true; + } + }) +} + function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { adUnit.bids[bidId].forEach(function(bid) { + let adapterName = getAdapterNameForAlias(bid.adapterCode || bid.bidder); + if (isOWPubmaticBid(adapterName) && isS2SBidder(bid.bidder)) { + return; + } + const pg = window.parseFloat(Number(bid.bidResponse?.adserverTargeting?.hb_pb || bid.bidResponse?.adserverTargeting?.pwtpb).toFixed(BID_PRECISION)); partnerBids.push({ - 'pn': getAdapterNameForAlias(bid.adapterCode || bid.bidder), + 'pn': adapterName, 'bc': bid.bidderCode || bid.bidder, 'bidid': bid.bidId || bidId, 'db': bid.bidResponse ? 0 : 1, @@ -270,9 +290,10 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, 'en': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, - 'di': bid.bidResponse ? (bid.bidResponse.dealId || EMPTY_STRING) : EMPTY_STRING, + 'di': bid.bidResponse ? (bid.bidResponse.dealId || OPEN_AUCTION_DEAL_ID) : OPEN_AUCTION_DEAL_ID, 'dc': bid.bidResponse ? (bid.bidResponse.dealChannel || EMPTY_STRING) : EMPTY_STRING, - 'l1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, + 'l1': bid.bidResponse ? bid.partnerTimeToRespond : 0, + 'ol1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, 'l2': 0, 'adv': bid.bidResponse ? getAdDomain(bid.bidResponse) || undefined : undefined, 'ss': isS2SBidder(bid.bidder), @@ -283,8 +304,9 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, - 'frv': (s2sBidders.indexOf(bid.bidder) > -1) ? undefined : (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), - 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined + 'frv': bid.bidResponse ? bid.bidResponse.floorData?.floorRuleValue : undefined, + 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined, + 'pb': pg || undefined }); }); return partnerBids; @@ -319,11 +341,24 @@ function getTgId() { return 0; } +function getFloorFetchStatus(floorData) { + if (!floorData?.floorRequestData) { + return false; + } + const { location, fetchStatus } = floorData?.floorRequestData; + const isDataValid = location !== CONSTANTS.FLOOR_VALUES.NO_DATA; + const isFetchSuccessful = location === CONSTANTS.FLOOR_VALUES.FETCH && fetchStatus === CONSTANTS.FLOOR_VALUES.SUCCESS; + const isAdUnitOrSetConfig = location === CONSTANTS.FLOOR_VALUES.AD_UNIT || location === CONSTANTS.FLOOR_VALUES.SET_CONFIG; + return isDataValid && (isAdUnitOrSetConfig || isFetchSuccessful); +} + function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let auctionCache = cache.auctions[auctionId]; - let floorData = auctionCache.floorData; + let wiid = auctionCache?.wiid || auctionId; + let floorData = auctionCache?.floorData; + let floorFetchStatus = getFloorFetchStatus(auctionCache?.floorData); let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; @@ -337,7 +372,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { pixelURL += 'pubid=' + publisherId; outputObj['pubid'] = '' + publisherId; - outputObj['iid'] = '' + auctionId; + outputObj['iid'] = '' + wiid; outputObj['to'] = '' + auctionCache.timeout; outputObj['purl'] = referrer; outputObj['orig'] = getDomainFromUrl(referrer); @@ -346,8 +381,9 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['pdvid'] = '' + profileVersionId; outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = getTgId(); + outputObj['pbv'] = getGlobal()?.version || '-1'; - if (floorData) { + if (floorData && floorFetchStatus) { outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; } @@ -358,12 +394,29 @@ function executeBidsLoggerCall(e, highestCpmBids) { // getGptSlotInfoForAdUnitCode returns gptslot corresponding to adunit provided as input. let slotObject = { 'sn': adUnitId, - 'au': origAdUnit.adUnitId || getGptSlotInfoForAdUnitCode(adUnitId)?.gptSlot || adUnitId, + 'au': origAdUnit.owAdUnitId || getGptSlotInfoForAdUnitCode(adUnitId)?.gptSlot || adUnitId, 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), - 'fskp': floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'fskp': floorData && floorFetchStatus ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'sid': generateUUID() }; + if (floorData?.floorRequestData) { + const { location, fetchStatus, floorProvider } = floorData?.floorRequestData; + slotObject.ffs = { + [CONSTANTS.FLOOR_VALUES.SUCCESS]: 1, + [CONSTANTS.FLOOR_VALUES.ERROR]: 2, + [CONSTANTS.FLOOR_VALUES.TIMEOUT]: 4, + undefined: 0 + }[fetchStatus]; + slotObject.fsrc = { + [CONSTANTS.FLOOR_VALUES.FETCH]: 2, + [CONSTANTS.FLOOR_VALUES.NO_DATA]: 2, + [CONSTANTS.FLOOR_VALUES.AD_UNIT]: 1, + [CONSTANTS.FLOOR_VALUES.SET_CONFIG]: 1 + }[location]; + slotObject.fp = floorProvider; + } slotsArray.push(slotObject); return slotsArray; }, []); @@ -385,36 +438,48 @@ function executeBidsLoggerCall(e, highestCpmBids) { function executeBidWonLoggerCall(auctionId, adUnitId) { const winningBidId = cache.auctions[auctionId].adUnitCodes[adUnitId].bidWon; const winningBids = cache.auctions[auctionId].adUnitCodes[adUnitId].bids[winningBidId]; - let winningBid = winningBids[0]; + if (!winningBids) { + logWarn(LOG_PRE_FIX + 'Could not find winningBids for : ', auctionId); + return; + } + let winningBid = winningBids[0]; if (winningBids.length > 1) { winningBid = winningBids.filter(bid => bid.adId === cache.auctions[auctionId].adUnitCodes[adUnitId].bidWonAdId)[0]; } const adapterName = getAdapterNameForAlias(winningBid.adapterCode || winningBid.bidder); + if (isOWPubmaticBid(adapterName) && isS2SBidder(winningBid.bidder)) { + return; + } let origAdUnit = getAdUnit(cache.auctions[auctionId].origAdUnits, adUnitId) || {}; + let owAdUnitId = origAdUnit.owAdUnitId || getGptSlotInfoForAdUnitCode(adUnitId)?.gptSlot || adUnitId; let auctionCache = cache.auctions[auctionId]; let floorData = auctionCache.floorData; + let wiid = cache.auctions[auctionId]?.wiid || auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let adv = winningBid.bidResponse ? getAdDomain(winningBid.bidResponse) || undefined : undefined; let fskp = floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined; - + let pg = window.parseFloat(Number(winningBid?.bidResponse?.adserverTargeting?.hb_pb || winningBid?.bidResponse?.adserverTargeting?.pwtpb)) || undefined; let pixelURL = END_POINT_WIN_BID_LOGGER; + pixelURL += 'pubid=' + publisherId; pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); pixelURL += '&tst=' + Math.round((new window.Date()).getTime() / 1000); - pixelURL += '&iid=' + enc(auctionId); + pixelURL += '&iid=' + enc(wiid); pixelURL += '&bidid=' + enc(winningBidId); pixelURL += '&pid=' + enc(profileId); pixelURL += '&pdvid=' + enc(profileVersionId); pixelURL += '&slot=' + enc(adUnitId); - pixelURL += '&au=' + enc(origAdUnit.adUnitId || adUnitId); + pixelURL += '&au=' + enc(owAdUnitId); pixelURL += '&pn=' + enc(adapterName); pixelURL += '&bc=' + enc(winningBid.bidderCode || winningBid.bidder); pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); + pixelURL += '&di=' + enc(winningBid?.bidResponse?.dealId || OPEN_AUCTION_DEAL_ID); + pixelURL += '&pb=' + enc(pg); pixelURL += '&plt=' + enc(getDevicePlatform()); pixelURL += '&psz=' + enc((winningBid?.bidResponse?.dimensions?.width || '0') + 'x' + @@ -443,7 +508,10 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { function auctionInitHandler(args) { s2sBidders = (function() { let s2sConf = config.getConfig('s2sConfig'); - return (s2sConf && isArray(s2sConf.bidders)) ? s2sConf.bidders : []; + let s2sBidders = []; + (s2sConf || []) && + isArray(s2sConf) ? s2sConf.map(conf => s2sBidders.push(...conf.bidders)) : s2sBidders.push(...s2sConf.bidders); + return s2sBidders || []; }()); let cacheEntry = pick(args, [ 'timestamp', @@ -466,6 +534,9 @@ function bidRequestedHandler(args) { dimensions: bid.sizes }; } + if (bid.bidder === 'pubmatic' && !!bid?.params?.wiid) { + cache.auctions[args.auctionId].wiid = bid.params.wiid; + } cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId] = [copyRequiredBidDetails(bid)]; if (bid.floorData) { cache.auctions[args.auctionId].floorData['floorRequestData'] = bid.floorData; @@ -474,6 +545,10 @@ function bidRequestedHandler(args) { } function bidResponseHandler(args) { + if (!args.requestId) { + logWarn(LOG_PRE_FIX + 'Got null requestId in bidResponseHandler'); + return; + } let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId][0]; if (!bid) { logError(LOG_PRE_FIX + 'Could not find associated bid request for bid response with requestId: ', args.requestId); @@ -492,10 +567,24 @@ function bidResponseHandler(args) { bid.adId = args.adId; bid.source = formatSource(bid.source || args.source); setBidStatus(bid, args); + const latency = args?.timeToRespond || Date.now() - cache.auctions[args.auctionId].timestamp; + const auctionTime = cache.auctions[args.auctionId].timeout; + // Check if latency is greater than auctiontime+150, then log auctiontime+150 to avoid large numbers + bid.partnerTimeToRespond = latency > (auctionTime + 150) ? (auctionTime + 150) : latency; bid.clientLatencyTimeMs = Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args); } +function bidRejectedHandler(args) { + // If bid is rejected due to floors value did not met + // make cpm as 0, status as bidRejected and forward the bid for logging + if (args.rejectionReason === CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET) { + args.cpm = 0; + args.status = CONSTANTS.BID_STATUS.BID_REJECTED; + bidResponseHandler(args); + } +} + function bidderDoneHandler(args) { cache.auctions[args.auctionId].bidderDonePendingCount--; args.bids.forEach(bid => { @@ -524,7 +613,7 @@ function auctionEndHandler(args) { let highestCpmBids = getGlobal().getHighestCpmBids() || []; setTimeout(() => { executeBidsLoggerCall.call(this, args, highestCpmBids); - }, (cache.auctions[args.auctionId].bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); + }, (cache.auctions[args.auctionId]?.bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); } function bidTimeoutHandler(args) { @@ -594,6 +683,9 @@ let pubmaticAdapter = Object.assign({}, baseAdapter, { case CONSTANTS.EVENTS.BID_RESPONSE: bidResponseHandler(args); break; + case CONSTANTS.EVENTS.BID_REJECTED: + bidRejectedHandler(args) + break; case CONSTANTS.EVENTS.BIDDER_DONE: bidderDoneHandler(args); break; diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index ce0da73f751..f28feaa534d 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -1,10 +1,17 @@ -import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, convertTypes, uniques, isPlainObject, isInteger } from '../src/utils.js'; +import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, uniques, isPlainObject, isInteger, generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE, ADPOD } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { bidderSettings } from '../src/bidderSettings.js'; import CONSTANTS from '../src/constants.json'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -733,9 +740,9 @@ function _addImpressionFPD(imp, bid) { const ortb2 = {...deepAccess(bid, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { /** - * Prebid AdSlot - * @type {(string|undefined)} - */ + * Prebid AdSlot + * @type {(string|undefined)} + */ if (prop === 'pbadslot') { if (typeof ortb2[prop] === 'string' && ortb2[prop]) deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); } else if (prop === 'adserver') { @@ -757,6 +764,9 @@ function _addImpressionFPD(imp, bid) { deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); } }); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + gpid && deepSetValue(imp, `ext.gpid`, gpid); } function _addFloorFromFloorModule(impObj, bid) { @@ -1000,6 +1010,10 @@ export function prepareMetaObject(br, bid, seat) { br.meta.secondaryCatIds = bid.cat; br.meta.primaryCatId = bid.cat[0]; } + + if (bid.ext && bid.ext.dsa && Object.keys(bid.ext.dsa).length) { + br.meta.dsa = bid.ext.dsa; + } } export const spec = { @@ -1007,11 +1021,11 @@ export const spec = { gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** - * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: bid => { if (bid && bid.params) { if (!isStr(bid.params.publisherId)) { @@ -1060,7 +1074,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} - an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { @@ -1077,8 +1091,10 @@ export const spec = { var bid; var blockedIabCategories = []; var allowedIabCategories = []; + var wiid = generateUUID(); validBidRequests.forEach(originalBid => { + originalBid.params.wiid = originalBid.params.wiid || bidderRequest.auctionId || wiid; bid = deepClone(originalBid); bid.params.adSlot = bid.params.adSlot || ''; _parseAdSlot(bid); @@ -1150,10 +1166,7 @@ export const spec = { payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); payload.user.geo = {}; // TODO: fix lat and long to only come from request object, not params - payload.user.geo.lat = _parseSlotParam('lat', 0); - payload.user.geo.lon = _parseSlotParam('lon', 0); payload.user.yob = _parseSlotParam('yob', conf.yob); - payload.device.geo = payload.user.geo; payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); @@ -1208,26 +1221,48 @@ export const spec = { deepSetValue(payload, 'regs.coppa', 1); } + // dsa + if (bidderRequest?.ortb2?.regs?.ext?.dsa) { + deepSetValue(payload, 'regs.ext.dsa', bidderRequest.ortb2.regs.ext.dsa); + } + _handleEids(payload, validBidRequests); // First Party Data const commonFpd = (bidderRequest && bidderRequest.ortb2) || {}; - if (commonFpd.site) { + const { user, device, site, bcat, badv } = commonFpd; + if (site) { const { page, domain, ref } = payload.site; - mergeDeep(payload, {site: commonFpd.site}); + mergeDeep(payload, {site: site}); payload.site.page = page; payload.site.domain = domain; payload.site.ref = ref; } - if (commonFpd.user) { - mergeDeep(payload, {user: commonFpd.user}); + if (user) { + mergeDeep(payload, {user: user}); + } + if (badv) { + mergeDeep(payload, {badv: badv}); } - if (commonFpd.bcat) { - blockedIabCategories = blockedIabCategories.concat(commonFpd.bcat); + if (bcat) { + blockedIabCategories = blockedIabCategories.concat(bcat); } // check if fpd ortb2 contains device property with sua object - if (commonFpd.device?.sua) { - payload.device.sua = commonFpd.device?.sua; + if (device?.sua) { + payload.device.sua = device?.sua; + } + + if (device?.ext?.cdep) { + deepSetValue(payload, 'device.ext.cdep', device.ext.cdep); + } + + if (user?.geo && device?.geo) { + payload.device.geo = { ...payload.device.geo, ...device.geo }; + payload.user.geo = { ...payload.user.geo, ...user.geo }; + } else { + if (user?.geo || device?.geo) { + payload.user.geo = payload.device.geo = (user?.geo ? { ...payload.user.geo, ...user.geo } : { ...payload.user.geo, ...device.geo }); + } } if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) { @@ -1352,9 +1387,25 @@ export const spec = { }); }); } + let fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return { + bidId, + config: Object.assign({ + auctionSignals: {}, + }, cfg) + } + }); + return { + bids: bidResponses, + fledgeAuctionConfigs, + } + } } catch (error) { logError(error); } + return bidResponses; }, diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js index 6a5d866c76d..eca0c971050 100644 --- a/modules/pubwiseBidAdapter.js +++ b/modules/pubwiseBidAdapter.js @@ -6,6 +6,13 @@ import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const VERSION = '0.3.0'; const GVLID = 842; const NET_REVENUE = true; @@ -173,7 +180,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {validBidRequests} - an array of bids * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { diff --git a/modules/pubxBidAdapter.js b/modules/pubxBidAdapter.js index ee28d549475..60e5be2a321 100644 --- a/modules/pubxBidAdapter.js +++ b/modules/pubxBidAdapter.js @@ -55,8 +55,8 @@ export const spec = { /** * Determine which user syncs should occur * @param {object} syncOptions - * @param {array} serverResponses - * @returns {array} User sync pixels + * @param {Array} serverResponses + * @returns {Array} User sync pixels */ getUserSyncs: function (syncOptions, serverResponses) { const kwTag = document.getElementsByName('keywords'); diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 19a3c236942..e97e5505768 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -1,9 +1,10 @@ -import { deepAccess, getGptSlotInfoForAdUnitCode, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; +import { deepAccess, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 7297c931326..516254b358b 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -1,6 +1,7 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import {convertTypes, isArray} from '../src/utils.js'; +import {isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const DEFAULT_CURRENCY = 'USD'; const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; diff --git a/modules/pxyzBidAdapter.js b/modules/pxyzBidAdapter.js index 1ab432496a3..12bd04c744d 100644 --- a/modules/pxyzBidAdapter.js +++ b/modules/pxyzBidAdapter.js @@ -2,6 +2,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {isArray, logError, logInfo} from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'pxyz'; const URL = 'https://ads.playground.xyz/host-config/prebid?v=2'; const DEFAULT_CURRENCY = 'USD'; @@ -124,10 +129,13 @@ export const spec = { return bids; }, - getUserSyncs: function (syncOptions) { + getUserSyncs: function () { return [{ type: 'image', url: '//ib.adnxs.com/getuidnb?https://ads.playground.xyz/usersync?partner=appnexus&uid=$UID' + }, { + type: 'iframe', + url: '//rtb.gumgum.com/getuid/15801?r=https%3A%2F%2Fads.playground.xyz%2Fusersync%3Fpartner%3Dgumgum%26uid%3D' }]; } } diff --git a/modules/qortexRtdProvider.js b/modules/qortexRtdProvider.js new file mode 100644 index 00000000000..7aa30334756 --- /dev/null +++ b/modules/qortexRtdProvider.js @@ -0,0 +1,165 @@ +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { logWarn, mergeDeep, logMessage, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +let requestUrl; +let bidderArray; +let impressionIds; +let currentSiteContext; + +/** + * Init if module configuration is valid + * @param {Object} config Module configuration + * @returns {Boolean} + */ +function init (config) { + if (!config?.params?.groupId?.length > 0) { + logWarn('Qortex RTD module config does not contain valid groupId parameter. Config params: ' + JSON.stringify(config.params)) + return false; + } else { + initializeModuleData(config); + } + if (config?.params?.tagConfig) { + loadScriptTag(config) + } + return true; +} + +/** + * Processess prebid request and attempts to add context to ort2b fragments + * @param {Object} reqBidsConfig Bid request configuration object + * @param {Function} callback Called on completion + */ +function getBidRequestData (reqBidsConfig, callback) { + if (reqBidsConfig?.adUnits?.length > 0) { + getContext() + .then(contextData => { + setContextData(contextData) + addContextToRequests(reqBidsConfig) + callback(); + }) + .catch((e) => { + logWarn(e?.message); + callback(); + }); + } else { + logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig)) + callback(); + } +} + +/** + * determines whether to send a request to context api and does so if necessary + * @returns {Promise} ortb Content object + */ +export function getContext () { + if (!currentSiteContext) { + logMessage('Requesting new context data'); + return new Promise((resolve, reject) => { + const callbacks = { + success(text, data) { + const result = data.status === 200 ? JSON.parse(data.response)?.content : null; + resolve(result); + }, + error(error) { + reject(new Error(error)); + } + } + ajax(requestUrl, callbacks) + }) + } else { + logMessage('Adding Content object from existing context data'); + return new Promise(resolve => resolve(currentSiteContext)); + } +} + +/** + * Updates bidder configs with the response from Qortex context services + * @param {Object} reqBidsConfig Bid request configuration object + * @param {string[]} bidders Bidders specified in module's configuration + */ +export function addContextToRequests (reqBidsConfig) { + if (currentSiteContext === null) { + logWarn('No context data received at this time'); + } else { + const fragment = { site: {content: currentSiteContext} } + if (bidderArray?.length > 0) { + bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment})) + } else if (!bidderArray) { + mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment); + } else { + logWarn('Config contains an empty bidders array, unable to determine which bids to enrich'); + } + } +} + +/** + * Loads Qortex header tag using data passed from module config object + * @param {Object} config module config obtained during init + */ +export function loadScriptTag(config) { + const code = 'qortex'; + const groupId = config.params.groupId; + const src = 'https://tags.qortex.ai/bootstrapper' + const attr = {'data-group-id': groupId} + const tc = config.params.tagConfig + + Object.keys(tc).forEach(p => { + attr[`data-${p.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`)}`] = tc[p] + }) + + addEventListener('qortex-rtd', (e) => { + const billableEvent = { + vendor: code, + billingId: generateUUID(), + type: e?.detail?.type, + accountId: groupId + } + switch (e?.detail?.type) { + case 'qx-impression': + const {uid} = e.detail; + if (!uid || impressionIds.has(uid)) { + logWarn(`received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`) + return; + } else { + logMessage('received billable event: qx-impression') + impressionIds.add(uid) + billableEvent.transactionId = e.detail.uid; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, billableEvent); + break; + } + default: + logWarn(`received invalid billable event: ${e.detail?.type}`) + } + }) + + loadExternalScript(src, code, undefined, undefined, attr); +} + +/** + * Helper function to set initial values when they are obtained by init + * @param {Object} config module config obtained during init + */ +export function initializeModuleData(config) { + const DEFAULT_API_URL = 'https://demand.qortex.ai'; + const {apiUrl, groupId, bidders} = config.params; + requestUrl = `${apiUrl || DEFAULT_API_URL}/api/v1/analyze/${groupId}/prebid`; + bidderArray = bidders; + impressionIds = new Set(); + currentSiteContext = null; +} + +export function setContextData(value) { + currentSiteContext = value +} + +export const qortexSubmodule = { + name: 'qortex', + init, + getBidRequestData +} + +submodule('realTimeData', qortexSubmodule); diff --git a/modules/qortexRtdProvider.md b/modules/qortexRtdProvider.md new file mode 100644 index 00000000000..312696068cd --- /dev/null +++ b/modules/qortexRtdProvider.md @@ -0,0 +1,69 @@ +# Qortex Real-time Data Submodule + +## Overview + +``` +Module Name: Qortex RTD Provider +Module Type: RTD Provider +Maintainer: mannese@qortex.ai +``` + +## Description + +The Qortex RTD module appends contextual segments to the bidding object based on the content of a page using the Qortex API. + +Upon load, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data. + + +## Build +``` +gulp build --modules="rtdModule,qortexRtdProvider,qortexBidAdapter,..." +``` + +> `rtdModule` is a required module to use Qortex RTD module. + +## Configuration + +Please refer to [Prebid Documentation](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-realTimeData) on RTD module configuration for details on required and optional parameters of `realTimeData` + +When configuring Qortex as a data provider, refer to the template below to add the necessary information to ensure the proper connection is made. + +### RTD Module Setup + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: 'qortex', + waitForIt: true, + params: { + groupId: 'ABC123', //required + bidders: ['qortex', 'adapter2'], //optional (see below) + tagConfig: { // optional, please reach out to your account manager for configuration reccommendation + videoContainer: 'string', + htmlContainer: 'string', + attachToTop: 'string', + esm6Mod: 'string', + continuousLoad: 'string' + } + } + }] + } +}); +``` + +### Paramter Details + +#### `groupId` - Required +- The Qortex groupId linked to the publisher, this is required to make a request using this adapter + +#### `bidders` - optional +- If this parameter is included, it must be an array of the strings that match the bidder code of the prebid adapters you would like this module to impact. `ortb2.site.content` will be updated *only* for adapters in this array + +- If this parameter is omitted, the RTD module will default to updating `ortb2.site.content` on *all* bid adapters being used on the page + +#### `tagConfig` - optional +- This optional parameter is an object containing the config settings that could be usedto initialize the Qortex integration on your page. A preconfigured object for this step will be provided to you by the Qortex team. + +- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. \ No newline at end of file diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 2c721a61616..1ba23302367 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -6,6 +6,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {parseDomain} from '../src/refererDetection.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'quantcast'; const DEFAULT_BID_FLOOR = 0.0000000001; diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 2faf638fc0b..d980f5316e5 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -11,6 +11,10 @@ import { triggerPixel, logInfo } from '../src/utils.js'; import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + */ + const QUANTCAST_FPA = '__qca'; const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) const DAY_MS = 86400000; diff --git a/modules/r2b2BidAdapter.js b/modules/r2b2BidAdapter.js new file mode 100644 index 00000000000..15a65e3924c --- /dev/null +++ b/modules/r2b2BidAdapter.js @@ -0,0 +1,309 @@ +import {logWarn, logError, triggerPixel, deepSetValue, getParameterByName} from '../src/utils.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {Renderer} from '../src/Renderer.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {pbsExtensions} from '../libraries/pbsExtensions/pbsExtensions.js'; +import {bidderSettings} from '../src/bidderSettings.js'; + +const ADAPTER_VERSION = '1.0.0'; +const BIDDER_CODE = 'r2b2'; +const GVL_ID = 1235; + +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TTL = 360; +const DEFAULT_NET_REVENUE = true; +const DEBUG_PARAM = 'pbjs_test_r2b2'; +const RENDERER_URL = 'https://delivery.r2b2.io/static/rendering.js'; + +const ENDPOINT = bidderSettings.get(BIDDER_CODE, 'endpoint') || 'hb.r2b2.cz'; +const SERVER_URL = 'https://' + ENDPOINT; +const URL_BID = SERVER_URL + '/openrtb2/bid'; +const URL_SYNC = SERVER_URL + '/cookieSync'; +const URL_EVENT = SERVER_URL + '/event'; + +const URL_EVENT_ON_BIDDER_ERROR = URL_EVENT + '/bidError'; +const URL_EVENT_ON_TIMEOUT = URL_EVENT + '/timeout'; + +const R2B2_TEST_UNIT = 'selfpromo'; + +export const internal = { + placementsToSync: [], + mappedParams: {} +} + +let r2b2Error = function(message, params) { + logError(message, params, BIDDER_CODE) +} + +function getIdParamsFromPID(pid) { + // selfpromo test creative + if (pid === R2B2_TEST_UNIT) { + return { d: 'test', g: 'test', p: 'selfpromo', m: 0, selfpromo: 1 } + } + if (!isNaN(pid)) { + return { pid: Number(pid) } + } + if (typeof pid === 'string') { + const params = pid.split('/'); + if (params.length === 3 || params.length === 4) { + const paramNames = ['d', 'g', 'p', 'm']; + return paramNames.reduce((p, paramName, index) => { + let param = params[index]; + if (paramName === 'm') { + param = ['desktop', 'classic', '0'].includes(param) ? 0 : Number(!!param) + } + p[paramName] = param; + return p + }, {}); + } + } +} + +function pickIdFromParams(params) { + if (!params) return null; + const { d, g, p, m, pid } = params; + return d ? { d, g, p, m } : { pid }; +} + +function getIdsFromBids(bids) { + return bids.reduce((ids, bid) => { + const params = internal.mappedParams[bid.bidId]; + const id = pickIdFromParams(params); + if (id) { + ids.push(id); + } + return ids + }, []); +} + +function triggerEvent(eventUrl, ids) { + if (ids && !ids.length) return; + const timeStamp = new Date().getTime(); + const symbol = (eventUrl.indexOf('?') === -1 ? '?' : '&'); + const url = eventUrl + symbol + `p=${btoa(JSON.stringify(ids))}&cb=${timeStamp}`; + triggerPixel(url) +} + +const converter = ortbConverter({ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const idParams = getIdParamsFromPID(bidRequest.params.pid); + deepSetValue(imp, 'ext.r2b2', idParams); + internal.placementsToSync.push(idParams); + internal.mappedParams[imp.id] = Object.assign({}, bidRequest.params, idParams); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'ext.version', ADAPTER_VERSION); + request.cur = [DEFAULT_CURRENCY]; + const test = getParameterByName(DEBUG_PARAM) === '1' ? 1 : 0; + deepSetValue(request, 'test', test); + return request; + }, + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL + }, + processors: pbsExtensions +}); + +function setUpRenderer(adUnitCode, bid) { + // let renderer load once in main window, but pass the renderDocument + let renderDoc; + const config = { + documentResolver: (bid, sourceDocument, renderDocument) => { + renderDoc = renderDocument; + return sourceDocument; + } + } + let renderer = Renderer.install({ + url: RENDERER_URL, + config: config, + id: bid.requestId, + adUnitCode + }); + + renderer.setRender(function (bid, doc) { + doc = renderDoc || doc; + window.R2B2 = window.R2B2 || {}; + let main = window.R2B2; + main.HB = main.HB || {}; + main.HB.Render = main.HB.Render || {}; + main.HB.Render.queue = main.HB.Render.queue || []; + main.HB.Render.queue.push(() => { + const id = pickIdFromParams(internal.mappedParams[bid.requestId]) + main.HB.Renderer.render(id, bid, null, doc) + }) + }) + + return renderer +} + +function getExtMediaType(bidMediaType, responseBid) { + switch (bidMediaType) { + case BANNER: + return { + type: 'banner', + settings: { + chd: null, + width: responseBid.w, + height: responseBid.h, + ad: { + type: 'content', + data: responseBid.adm + } + } + }; + case NATIVE: + break; + case VIDEO: + break; + default: + break; + } +} + +function createPrebidResponseBid(requestImp, bidResponse, serverResponse, bids) { + const bidId = requestImp.id; + const adUnitCode = bids[0].adUnitCode; + const mediaType = bidResponse.ext.prebid.type; + let bidOut = { + requestId: bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + width: bidResponse.w, + height: bidResponse.h, + ttl: bidResponse.ttl ?? DEFAULT_TTL, + netRevenue: serverResponse.netRevenue ?? DEFAULT_NET_REVENUE, + currency: serverResponse.cur ?? DEFAULT_CURRENCY, + ad: bidResponse.adm, + mediaType: mediaType, + winUrl: bidResponse.nurl, + ext: { + cid: bidResponse.ext?.r2b2?.cid, + cdid: bidResponse.ext?.r2b2?.cdid, + mediaType: getExtMediaType(mediaType, bidResponse), + adUnit: adUnitCode, + dgpm: internal.mappedParams[bidId], + events: bidResponse.ext?.r2b2?.events + } + }; + if (bidResponse.ext?.r2b2?.useRenderer) { + bidOut.renderer = setUpRenderer(adUnitCode, bidOut); + } + return bidOut; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + if (!bid.params || !bid.params.pid) { + logWarn('Bad params, "pid" required.'); + return false + } + const id = getIdParamsFromPID(bid.params.pid); + if (!id || !(id.pid || (id.d && id.g && id.p))) { + logWarn('Bad params, "pid" has to be either a number or a correctly assembled string.'); + return false + } + return true + }, + buildRequests: function(validBidRequests, bidderRequest) { + const data = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest + }); + return [{ + method: 'POST', + url: URL_BID, + data, + bids: bidderRequest.bids + }] + }, + + interpretResponse: function(serverResponse, request) { + // r2b2Error('error message', {params: 1}); + let prebidResponses = []; + + const response = serverResponse.body; + if (!response || !response.seatbid || !response.seatbid[0] || !response.seatbid[0].bid) { + return prebidResponses; + } + let requestImps = request.data.imp || []; + try { + response.seatbid.forEach(seat => { + let bids = seat.bid; + + for (let responseBid of bids) { + let responseImpId = responseBid.impid; + let requestCurrentImp = requestImps.find((requestImp) => requestImp.id === responseImpId); + if (!requestCurrentImp) { + r2b2Error('Cant match bid response.', {impid: Boolean(responseBid.impid)}); + continue;// Skip this iteration if there's no match + } + prebidResponses.push(createPrebidResponseBid(requestCurrentImp, responseBid, response, request.bids)); + } + }) + } catch (e) { + r2b2Error('Error while interpreting response:', {msg: e.message}); + } + return prebidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + if (!syncOptions.iframeEnabled) { + logWarn('Please enable iframe based user sync.'); + return syncs; + } + + let plString; + try { + plString = btoa(JSON.stringify(internal.placementsToSync || [])); + } catch (e) { + logWarn('User sync failed: ' + e.message); + return syncs + } + + let url = URL_SYNC + `?p=${plString}`; + + if (gdprConsent) { + url += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + } + + if (uspConsent) { + url += `&us_privacy=${uspConsent}` + } + + syncs.push({ + type: 'iframe', + url: url + }) + return syncs; + }, + onBidWon: function(bid) { + const url = bid.ext?.events?.onBidWon; + if (url) { + triggerEvent(url) + } + }, + onSetTargeting: function(bid) { + const url = bid.ext?.events?.onSetTargeting; + if (url) { + triggerEvent(url) + } + }, + onTimeout: function(bids) { + triggerEvent(URL_EVENT_ON_TIMEOUT, getIdsFromBids(bids)) + }, + onBidderError: function(params) { + let { bidderRequest } = params; + triggerEvent(URL_EVENT_ON_BIDDER_ERROR, getIdsFromBids(bidderRequest.bids)) + } +} +registerBidder(spec); diff --git a/modules/r2b2BidAdapter.md b/modules/r2b2BidAdapter.md new file mode 100644 index 00000000000..43b59133215 --- /dev/null +++ b/modules/r2b2BidAdapter.md @@ -0,0 +1,37 @@ +# Overview + +``` +Module Name: R2B2 Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@r2b2.cz +``` + +## Description + +Module that integrates R2B2 demand sources. To get your bidder configuration reach out to our account team on partner@r2b2.io + + + +## Test unit + +```javascript + var adUnits = [ + { + code: 'test-r2b2', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'r2b2', + params: { + pid: 'selfpromo' + } + }] + } + ]; +``` +## Rendering + +Our adapter can feature a custom renderer specifically for display ads, tailored to enhance ad presentation and functionality. This is particularly beneficial for non-standard ad formats that require more complex logic. It's important to note that our rendering process operates outside of SafeFrames. For additional information, not limited to rendering aspects, please feel free to contact us at partner@r2b2.io diff --git a/modules/radsBidAdapter.js b/modules/radsBidAdapter.js index ae16bcf9d83..faa35ee51f7 100644 --- a/modules/radsBidAdapter.js +++ b/modules/radsBidAdapter.js @@ -2,6 +2,10 @@ import {deepAccess} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'rads'; const ENDPOINT_URL = 'https://rads.recognified.net/md.request.php'; const ENDPOINT_URL_DEV = 'https://dcradn1.online-solution.biz/md.request.php'; diff --git a/modules/rasBidAdapter.js b/modules/rasBidAdapter.js index a7aceb107b9..74abd0fb4a1 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,7 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { isEmpty, getAdUnitSizes, parseSizesInput, deepAccess } from '../src/utils.js'; -import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { + isEmpty, + parseSizesInput, + deepAccess +} from '../src/utils.js'; +import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import { getAdUnitSizes } from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ras'; const VERSION = '1.0'; @@ -55,28 +60,156 @@ function parseParams(params, bidderRequest) { } } } + if (bidderRequest?.ortb2?.regs?.ext?.dsa?.required !== undefined) { + newParams.dsainfo = bidderRequest?.ortb2?.regs?.ext?.dsa?.required; + } return newParams; } -const buildBid = (ad) => { - if (ad.type === 'empty') { +/** + * @param url string + * @param type number // 1 - img, 2 - js + * @returns an object { event: 1, method: 1 or 2, url: 'string' } + */ +function prepareItemEventtrackers(url, type) { + return { + event: 1, + method: type, + url: url + }; +} + +function prepareEventtrackers(emsLink, imp, impression, impression1, impressionJs1) { + const eventtrackers = [prepareItemEventtrackers(emsLink, 1)]; + + if (imp) { + eventtrackers.push(prepareItemEventtrackers(imp, 1)); + } + + if (impression) { + eventtrackers.push(prepareItemEventtrackers(impression, 1)); + } + + if (impression1) { + eventtrackers.push(prepareItemEventtrackers(impression1, 1)); + } + + if (impressionJs1) { + eventtrackers.push(prepareItemEventtrackers(impressionJs1, 2)); + } + + return eventtrackers; +} + +function parseOrtbResponse(ad) { + if (!(ad.data?.fields && ad.data?.meta)) { + return false; + } + + const { image, Image, title, url, Headline, Thirdpartyclicktracker, imp, impression, impression1, impressionJs1 } = ad.data.fields; + const { dsaurl, height, width, adclick } = ad.data.meta; + const emsLink = ad.ems_link; + const link = adclick + (url || Thirdpartyclicktracker); + const eventtrackers = prepareEventtrackers(emsLink, imp, impression, impression1, impressionJs1); + const ortb = { + ver: '1.2', + assets: [ + { + id: 2, + img: { + url: image || Image || '', + w: width, + h: height + } + }, + { + id: 4, + title: { + text: title || Headline || '' + } + }, + { + id: 3, + data: { + value: deepAccess(ad, 'data.meta.advertiser_name', null), + type: 1 + } + } + ], + link: { + url: link + }, + eventtrackers + }; + + if (dsaurl) { + ortb.privacy = dsaurl + } + + return ortb +} + +function parseNativeResponse(ad) { + if (!(ad.data?.fields && ad.data?.meta)) { + return false; + } + + const { image, Image, title, leadtext, url, Calltoaction, Body, Headline, Thirdpartyclicktracker } = ad.data.fields; + const { dsaurl, height, width, adclick } = ad.data.meta; + const link = adclick + (url || Thirdpartyclicktracker); + const nativeResponse = { + sendTargetingKeys: false, + title: title || Headline || '', + image: { + url: image || Image || '', + width, + height + }, + + clickUrl: link, + cta: Calltoaction || '', + body: leadtext || Body || '', + sponsoredBy: deepAccess(ad, 'data.meta.advertiser_name', null) || '', + ortb: parseOrtbResponse(ad) + }; + + if (dsaurl) { + nativeResponse.privacyLink = dsaurl; + } + + return nativeResponse +} + +const buildBid = (ad, mediaType) => { + if (ad.type === 'empty' || mediaType === undefined) { return null; } - return { + + const data = { requestId: ad.id, cpm: ad.bid_rate ? ad.bid_rate.toFixed(2) : 0, - width: ad.width || 0, - height: ad.height || 0, ttl: 300, creativeId: ad.adid ? parseInt(ad.adid.split(',')[2], 10) : 0, netRevenue: true, currency: ad.currency || 'USD', dealId: null, - meta: { - mediaType: BANNER - }, - ad: ad.html || null - }; + actgMatch: ad.actg_match || 0, + meta: { mediaType: BANNER }, + mediaType: BANNER, + ad: ad.html || null, + width: ad.width || 0, + height: ad.height || 0 + } + + if (mediaType === 'native') { + data.meta = { mediaType: NATIVE }; + data.mediaType = NATIVE; + data.native = parseNativeResponse(ad) || {}; + + delete data.ad; + } + + return data; }; const getContextParams = (bidRequests, bidderRequest) => { @@ -101,18 +234,24 @@ const getSlots = (bidRequests) => { for (let i = 0; i < batchSize; i++) { const adunit = bidRequests[i]; const slotSequence = deepAccess(adunit, 'params.slotSequence'); - - const sizes = parseSizesInput(getAdUnitSizes(adunit)).join(','); + const creFormat = getAdUnitCreFormat(adunit); + const sizes = creFormat === 'native' ? 'fluid' : parseSizesInput(getAdUnitSizes(adunit)).join(','); queryString += `&slot${i}=${encodeURIComponent(adunit.params.slot)}&id${i}=${encodeURIComponent(adunit.bidId)}&composition${i}=CHILD`; - if (sizes.length) { + if (creFormat === 'native') { + queryString += `&cre_format${i}=native`; + } + + if (sizes) { queryString += `&iusizes${i}=${encodeURIComponent(sizes)}`; } - if (slotSequence !== undefined) { + + if (slotSequence !== undefined && slotSequence !== null) { queryString += `&pos${i}=${encodeURIComponent(slotSequence)}`; } } + return queryString; }; @@ -129,9 +268,54 @@ const getGdprParams = (bidderRequest) => { return queryString; }; +const parseAuctionConfigs = (serverResponse, bidRequest) => { + if (isEmpty(bidRequest)) { + return null; + } + const auctionConfigs = []; + const gctx = serverResponse && serverResponse.body?.gctx; + + bidRequest.bidIds.filter(bid => bid.fledgeEnabled).forEach((bid) => { + auctionConfigs.push({ + 'bidId': bid.bidId, + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': `https://csr.onet.pl/${encodeURIComponent(bid.params.network)}/v1/protected-audience-api/decision-logic.js`, + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': bid.params, + 'sizes': bid.sizes, + 'gctx': gctx + } + } + }); + }); + + if (auctionConfigs.length === 0) { + return null; + } else { + return auctionConfigs; + } +} + +const getAdUnitCreFormat = (adUnit) => { + if (!adUnit) { + return; + } + + let creFormat = 'html'; + let mediaTypes = Object.keys(adUnit.mediaTypes); + + if (mediaTypes && mediaTypes.length === 1 && mediaTypes.includes('native')) { + creFormat = 'native'; + } + + return creFormat; +} + export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, NATIVE], isBidRequestValid: function (bidRequest) { if (!bidRequest || !bidRequest.params || typeof bidRequest.params !== 'object') { @@ -145,8 +329,17 @@ export const spec = { const slotsQuery = getSlots(bidRequests); const contextQuery = getContextParams(bidRequests, bidderRequest); const gdprQuery = getGdprParams(bidderRequest); - const bidIds = bidRequests.map((bid) => ({ slot: bid.params.slot, bidId: bid.bidId })); + const fledgeEligible = Boolean(bidderRequest && bidderRequest.fledgeEnabled); const network = bidRequests[0].params.network; + const bidIds = bidRequests.map((bid) => ({ + slot: bid.params.slot, + bidId: bid.bidId, + sizes: getAdUnitSizes(bid), + params: bid.params, + fledgeEnabled: fledgeEligible, + mediaType: (bid.mediaTypes && bid.mediaTypes.banner) ? 'display' : NATIVE + })); + return [{ method: 'GET', url: getEndpoint(network) + contextQuery + slotsQuery + gdprQuery, @@ -156,10 +349,18 @@ export const spec = { interpretResponse: function (serverResponse, bidRequest) { const response = serverResponse.body; - if (!response || !response.ads || response.ads.length === 0) { - return []; + const fledgeAuctionConfigs = parseAuctionConfigs(serverResponse, bidRequest); + const bids = (!response || !response.ads || response.ads.length === 0) ? [] : response.ads.map((ad, index) => buildBid( + ad, + bidRequest?.bidIds?.[index]?.mediaType || 'banner' + )).filter((bid) => !isEmpty(bid)); + + if (fledgeAuctionConfigs) { + // Return a tuple of bids and auctionConfigs. It is possible that bids could be null. + return {bids, fledgeAuctionConfigs}; + } else { + return bids; } - return response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); } }; diff --git a/modules/rasBidAdapter.md b/modules/rasBidAdapter.md index e8a61974130..cf169fedb63 100644 --- a/modules/rasBidAdapter.md +++ b/modules/rasBidAdapter.md @@ -9,7 +9,7 @@ Maintainer: support@ringpublishing.com # Description Module that connects to Ringer Axel Springer demand sources. -Only banner format is supported. +Only banner and native format is supported. # Test Parameters ```js @@ -49,4 +49,4 @@ var adUnits = [{ | pageContext.keyValues | optional | Object | Key-values associated with this ad unit (case-insensitive); following characters are not allowed in the values: `" ' = ! + # * ~ ; ^ ( ) < > [ ] & @` | `{}` | | pageContext.keyValues.ci | optional | String | Content unique identifier | `"932016a5-02fc-4d5c-b643-fafc2f270f06"` | | pageContext.keyValues.adunit | optional | String | Ad unit name | `"example_com/sport"` | -| customParams | optional | Object | Custom request params | `{}` | \ No newline at end of file +| customParams | optional | Object | Custom request params | `{}` | diff --git a/modules/raynRtdProvider.js b/modules/raynRtdProvider.js new file mode 100644 index 00000000000..d558c360c4a --- /dev/null +++ b/modules/raynRtdProvider.js @@ -0,0 +1,198 @@ +/** + * This module adds the Rayn provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch real-time audience and context data from Rayn + * @module modules/raynRtdProvider + * @requires module:modules/realTimeData + */ + +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess, deepSetValue, logError, logMessage, mergeDeep } from '../src/utils.js'; + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'rayn'; +const RAYN_TCF_ID = 1220; +const LOG_PREFIX = 'RaynJS: '; +export const SEGMENTS_RESOLVER = 'rayn.io'; +export const RAYN_LOCAL_STORAGE_KEY = 'rayn-segtax'; + +const defaultIntegration = { + iabAudienceCategories: { + v1_1: { + tier: 6, + enabled: true, + }, + }, + iabContentCategories: { + v3_0: { + tier: 4, + enabled: true, + }, + v2_2: { + tier: 4, + enabled: true, + }, + }, +}; + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: SUBMODULE_NAME, +}); + +function init(moduleConfig, userConsent) { + return true; +} + +/** + * Create and return ORTB2 object with segtax and segments + * @param {number} segtax + * @param {Array} segmentIds + * @param {number} maxTier + * @return {Array} + */ +export function generateOrtbDataObject(segtax, segment, maxTier) { + const segmentIds = []; + + try { + Object.keys(segment).forEach(tier => { + if (tier <= maxTier) { + segmentIds.push(...segment[tier].map((id) => { + return { id }; + })) + } + }); + } catch (error) { + logError(LOG_PREFIX, error); + } + + return { + name: SEGMENTS_RESOLVER, + ext: { + segtax, + }, + segment: segmentIds, + }; +} + +/** + * Generates checksum + * @param {string} url + * @returns {string} + */ +export function generateChecksum(stringValue) { + const l = stringValue.length; + let i = 0; + let h = 0; + if (l > 0) while (i < l) h = ((h << 5) - h + stringValue.charCodeAt(i++)) | 0; + return h.toString(); +}; + +/** + * Gets an object of segtax and segment IDs from LocalStorage + * or return the default value provided. + * @param {string} key + * @return {Object} + */ +export function readSegments(key) { + try { + return JSON.parse(storage.getDataFromLocalStorage(key)); + } catch (error) { + logError(LOG_PREFIX, error); + return null; + } +} + +/** + * Pass segments to configured bidders, using ORTB2 + * @param {Object} bidConfig + * @param {Array} bidders + * @param {Object} integrationConfig + * @param {Array} segments + * @return {void} + */ +export function setSegmentsAsBidderOrtb2(bidConfig, bidders, integrationConfig, segments, checksum) { + const raynOrtb2 = {}; + + const raynContentData = []; + if (integrationConfig.iabContentCategories.v2_2.enabled && segments[checksum] && segments[checksum][6]) { + raynContentData.push(generateOrtbDataObject(6, segments[checksum][6], integrationConfig.iabContentCategories.v2_2.tier)); + } + if (integrationConfig.iabContentCategories.v3_0.enabled && segments[checksum] && segments[checksum][7]) { + raynContentData.push(generateOrtbDataObject(7, segments[checksum][7], integrationConfig.iabContentCategories.v3_0.tier)); + } + if (raynContentData.length > 0) { + deepSetValue(raynOrtb2, 'site.content.data', raynContentData); + } + + if (integrationConfig.iabAudienceCategories.v1_1.enabled && segments[4]) { + const raynUserData = [generateOrtbDataObject(4, segments[4], integrationConfig.iabAudienceCategories.v1_1.tier)]; + deepSetValue(raynOrtb2, 'user.data', raynUserData); + } + + if (!bidders || bidders.length === 0 || !segments || Object.keys(segments).length <= 0) { + mergeDeep(bidConfig?.ortb2Fragments?.global, raynOrtb2); + } else { + const bidderConfig = Object.fromEntries( + bidders.map((bidder) => [bidder, raynOrtb2]), + ); + mergeDeep(bidConfig?.ortb2Fragments?.bidder, bidderConfig); + } +} + +/** + * Real-time data retrieval from Rayn + * @param {Object} reqBidsConfigObj + * @param {function} callback + * @param {Object} config + * @param {Object} userConsent + * @return {void} + */ +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + try { + const checksum = generateChecksum(window.location.href); + + const segments = readSegments(RAYN_LOCAL_STORAGE_KEY); + + const bidders = deepAccess(config, 'params.bidders'); + const integrationConfig = mergeDeep(defaultIntegration, deepAccess(config, 'params.integration')); + + if (segments && Object.keys(segments).length > 0 && ( + segments[checksum] || (segments[4] && + integrationConfig.iabAudienceCategories.v1_1.enabled && + !integrationConfig.iabContentCategories.v2_2.enabled && + !integrationConfig.iabContentCategories.v3_0.enabled + ) + )) { + logMessage(LOG_PREFIX, `Segtax data from localStorage: ${JSON.stringify(segments)}`); + setSegmentsAsBidderOrtb2(reqBidsConfigObj, bidders, integrationConfig, segments, checksum); + callback(); + } else if (window.raynJS && typeof window.raynJS.getSegtax === 'function') { + window.raynJS.getSegtax().then((segtaxData) => { + logMessage(LOG_PREFIX, `Segtax data from RaynJS: ${JSON.stringify(segtaxData)}`); + setSegmentsAsBidderOrtb2(reqBidsConfigObj, bidders, integrationConfig, segtaxData, checksum); + callback(); + }).catch((error) => { + logError(LOG_PREFIX, error); + callback(); + }); + } else { + logMessage(LOG_PREFIX, 'No segtax data'); + callback(); + } + } catch (error) { + logError(LOG_PREFIX, error); + callback(); + } +} + +export const raynSubmodule = { + name: SUBMODULE_NAME, + init: init, + getBidRequestData: alterBidRequests, + gvlid: RAYN_TCF_ID, +}; + +submodule(MODULE_NAME, raynSubmodule); diff --git a/modules/raynRtdProvider.md b/modules/raynRtdProvider.md new file mode 100644 index 00000000000..8d888a18d1f --- /dev/null +++ b/modules/raynRtdProvider.md @@ -0,0 +1,118 @@ +--- +layout: page_v2 +title: Rayn RTD Provider +display_name: Rayn Real Time Data Module +description: Rayn Real Time Data module appends privacy preserving enhanced contextual categories and audiences. Moments matter. +page_type: module +module_type: rtd +module_code: raynRtdProvider +enable_download: true +vendor_specific: true +sidebarType: 1 +--- + +# Rayn Real-time Data Submodule + +Rayn is a privacy preserving, data platform. We turn content into context, into audiences. For Personalisation, Monetisation and Insights. This module reads contextual categories and audience cohorts from RaynJS (via localStorage) and passes them to the bid-stream. + +## Integration + +To install the module, follow these instructions: + +Step 1: Prepare the base Prebid file +Compile the Rayn RTD module (`raynRtdProvider`) into your Prebid build along with the parent RTD Module (`rtdModule`). From the command line, run gulp build `gulp build --modules=rtdModule,raynRtdProvider` + +Step 2: Set configuration +Enable Rayn RTD Module using pbjs.setConfig. Example is provided in the Configuration section. See the **Parameter Description** for more detailed information of the configuration parameters. + +### Configuration + +This module is configured as part of the realTimeData.dataProviders object. + +Example format: + +```js +pbjs.setConfig( + // ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "rayn", + waitForIt: true, + params: { + bidders: ["appnexus", "pubmatic"], + integration: { + iabAudienceCategories: { + v1_1: { + tier: 6, + enabled: true, + }, + }, + iabContentCategories: { + v3_0: { + tier: 4, + enabled: true, + }, + v2_2: { + tier: 4, + enabled: true, + }, + }, + } + } + } + ] + } + // ... +} +``` + +## Parameter Description + +The parameters below provide configurability for general behaviours of the RTD submodule, as well as enabling settings for specific use cases mentioned above (e.g. tiers and bidders). + +### Parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Description | Notes | +| :---------------------------------------------------- | :-------- | :----------------------------------------------------------------------------------- | :---- | +| name | `String` | RTD sub module name | Always "rayn" | +| waitForIt | `Boolean` | Required to ensure that the auction is delayed for the module to respond | Optional. Defaults to false but recommended to true | +| params | `Object` | || +| params.bidders | `Array` | Bidders with which to share context and segment information | Optional. In case no bidder is specified Rayn will append data for all bidders | +| params.integration | `Object` | Controls which IAB taxonomy should be used and up to which category tier | Optional. In case it's not defined, all supported IAB taxonomies and all category tiers will be used | +| params.integration.iabAudienceCategories | `Object` | || +| params.integration.iabAudienceCategories.v1_1 | `Object` | || +| params.integration.iabAudienceCategories.v1_1.enabled | `Boolean` | Controls if IAB Audience Taxonomy v1.1 will be used | Optional. Enabled by default | +| params.integration.iabAudienceCategories.v1_1.tier | `Number` | Controls up to which IAB Audience Taxonomy v1.1 Category tier will be used | Optional. Tier 6 by default | +| params.integration.iabContentCategories | `Object` | || +| params.integration.iabContentCategories.v3_0 | `Object` | || +| params.integration.iabContentCategories.v3_0.enabled | `Boolean` | Controls if IAB Content Taxonomy v3.0 will be used | Optional. Enabled by default | +| params.integration.iabContentCategories.v3_0.tier | `Number` | Controls up to which IAB Content Taxonomy v3.0 Category tier will be used | Optional. Tier 4 by default | +| params.integration.iabContentCategories.v2_2 | `Object` | || +| params.integration.iabContentCategories.v2_2.enabled | `Boolean` | Controls if IAB Content Taxonomy v2.2 will be used | Optional. Enabled by default | +| params.integration.iabContentCategories.v2_2.tier | `Number` | Controls up to which IAB Content Taxonomy v2.2 Category tier will be used | Optional. Tier 4 by default | + +Please note that raynRtdProvider should be integrated into the website along with RaynJS. + +## Testing + +To view an example of the on page setup: + +```bash +gulp serve-fast --modules=rtdModule,raynRtdProvider,appnexusBidAdapter +``` + +Then in your browser access: [http://localhost:9999/integrationExamples/gpt/raynRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/raynRtdProvider_example.html) + +Run the unit tests, just on the Rayn RTD module test file: + +```bash +gulp test --file "test/spec/modules/raynRtdProvider_spec.js" +``` + +## Support + +If you require further assistance or are interested in discussing the module functionality please reach out to [support@rayn.io](mailto:support@rayn.io). +You are also able to find more examples and other integration routes on the Rayn documentation site. diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index 718d6504b56..4ff51aeb43e 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -349,7 +349,7 @@ function nativeResponse(imp, bid) { keys.cta = asset.data && asset.id === 5 ? asset.data.value : keys.cta; }); if (nativeAd.link) { - keys.clickUrl = encodeURIComponent(nativeAd.link.url); + keys.clickUrl = nativeAd.link.url; } const trackers = nativeAd.imptrackers || []; trackers.unshift(replaceAuctionPrice(bid.burl, bid.price)); diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index 9b6a3d7aca3..5671b2021d8 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -21,6 +21,10 @@ import {ajaxBuilder} from '../src/ajax.js'; import {generateUUID, isGptPubadsDefined, logError, timestamp} from '../src/utils.js'; import {find} from '../src/polyfill.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** @type {Object} */ const MessageType = { IMPRESSION_REQUEST: 'rsdk:impression:req', diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index b2961b09eb5..751e8fa442c 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -1,4 +1,14 @@ -import { deepAccess, logWarn, getBidIdParameter, parseQueryStringParameters, triggerPixel, generateUUID, isArray, isNumber, parseSizesInput } from '../src/utils.js'; +import { + deepAccess, + logWarn, + parseQueryStringParameters, + triggerPixel, + generateUUID, + isArray, + isNumber, + parseSizesInput, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; @@ -94,7 +104,8 @@ function buildRequests(validBidRequests, bidderRequest) { width: width, height: height, banner_sizes: getBannerSizes(bidRequest), - media_type: mediaType + media_type: mediaType, + userIdAsEids: bidRequest.userIdAsEids || {}, }); } @@ -107,6 +118,7 @@ function buildRequests(validBidRequests, bidderRequest) { uuid: getUuid(), pv: '$prebid.version$', imuid: imuid, + canonical_url: bidderRequest.refererInfo?.canonicalUrl || null, canonical_url_hash: getCanonicalUrlHash(bidderRequest.refererInfo), ref: bidderRequest.refererInfo.page }); @@ -132,6 +144,7 @@ function interpretResponse(serverResponse, bidRequest) { const playerUrl = res.playerUrl || bidRequest.player || body.playerUrl; let bidResponse = { requestId: res.bidId, + placementId: res.placementId, width: res.width, height: res.height, cpm: res.price, diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js new file mode 100644 index 00000000000..af145a5e163 --- /dev/null +++ b/modules/relayBidAdapter.js @@ -0,0 +1,99 @@ +import { isNumber, logMessage } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'relay'; +const METHOD = 'POST'; +const ENDPOINT_URL = 'https://e.relay.bid/p/openrtb2'; + +// The default impl from the prebid docs. +const CONVERTER = + ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } + }); + +function buildRequests(bidRequests, bidderRequest) { + const prebidVersion = config.getConfig('prebid_version') || 'v8.1.0'; + // Group bids by accountId param + const groupedByAccountId = bidRequests.reduce((accu, item) => { + const accountId = ((item || {}).params || {}).accountId; + if (!accu[accountId]) { accu[accountId] = []; }; + accu[accountId].push(item); + return accu; + }, {}); + // Send one overall request with all grouped bids per accountId + let reqs = []; + for (const [accountId, accountBidRequests] of Object.entries(groupedByAccountId)) { + const url = `${ENDPOINT_URL}?a=${accountId}&pb=1&pbv=${prebidVersion}`; + const data = CONVERTER.toORTB({ bidRequests: accountBidRequests, bidderRequest }) + const req = { + method: METHOD, + url, + data + }; + reqs.push(req); + } + return reqs; +}; + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ response: response.body, request: request.data }).bids; +}; + +function isBidRequestValid(bid) { + return isNumber((bid.params || {}).accountId); +}; + +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncs = [] + for (const response of serverResponses) { + const responseSyncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) + // Relay returns user_syncs in the format expected by prebid. If for any + // reason the request/response failed to properly capture the GDPR settings + // -- fallback to those identified by Prebid. + for (const sync of responseSyncs) { + const syncUrl = new URL(sync.url); + const missingGdpr = !syncUrl.searchParams.has('gdpr'); + const missingGdprConsent = !syncUrl.searchParams.has('gdpr_consent'); + if (missingGdpr) { + syncUrl.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) + sync.url = syncUrl.toString(); + } + if (missingGdprConsent) { + syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); + sync.url = syncUrl.toString(); + } + if (syncOptions.iframeEnabled && sync.type === 'iframe') { + syncs.push(sync); + } else if (syncOptions.pixelEnabled && sync.type === 'image') { + syncs.push(sync); + } + } + } + + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeout: function (timeoutData) { + logMessage('Timeout: ', timeoutData); + }, + onBidWon: function (bid) { + logMessage('Bid won: ', bid); + }, + onBidderError: function ({ error, bidderRequest }) { + logMessage('Error: ', error, bidderRequest); + }, + supportedMediaTypes: [BANNER, VIDEO, NATIVE] +} +registerBidder(spec); diff --git a/modules/relayBidAdapter.md b/modules/relayBidAdapter.md new file mode 100644 index 00000000000..882e04b7b13 --- /dev/null +++ b/modules/relayBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Relay Bid Adapter +Module Type: Bid Adapter +Maintainer: relay@kevel.co +``` + +# Description + +Connects to Relay exchange API for bids. +Supports Banner, Video and Native. + +# Test Parameters + +``` +var adUnits = [ + // Banner with minimal bid configuration + { + code: 'minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 1234 + } + } + } + } + } + } + ] + }, + // Minimal video + { + code: 'video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 'example' + } + } + } + } + } + } + ] + } +]; +``` diff --git a/modules/relevadRtdProvider.js b/modules/relevadRtdProvider.js index a7d0305da62..613eaa71a1f 100644 --- a/modules/relevadRtdProvider.js +++ b/modules/relevadRtdProvider.js @@ -110,6 +110,7 @@ function composeOrtb2Data(rtdData, prefix) { !isEmpty(categories.cat) && deepSetValue(addOrtb2, prefix + '.cat', categories.cat); !isEmpty(categories.pagecat) && deepSetValue(addOrtb2, prefix + '.pagecat', categories.pagecat); !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.sectioncat', categories.sectioncat); + !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.ext.data.relevad_rtd', categories.sectioncat); !isEmpty(categories.cattax) && deepSetValue(addOrtb2, prefix + '.cattax', categories.cattax); if (!isEmpty(content) && !isEmpty(content.segs) && content.segtax) { @@ -134,7 +135,8 @@ function setBidderSiteAndContent(bidderOrtbFragment, bidder, rtdData) { try { let addOrtb2 = composeOrtb2Data(rtdData, 'site'); !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.relevad_rtd', rtdData.segments); - !isEmpty(rtdData.categories?.sectioncat) && deepSetValue(addOrtb2, 'site.ext.data.relevad_rtd', rtdData.categories.sectioncat); + !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.segments', rtdData.segments); + !isEmpty(rtdData.categories) && deepSetValue(addOrtb2, 'user.ext.data.contextual_categories', rtdData.categories.pagecat); if (isEmpty(addOrtb2)) { return; } @@ -180,7 +182,7 @@ function getFiltered(data, minscore) { const cats = filterByScore(data.cats, minscore); const pcats = filterByScore(data.pcats, minscore) || cats; const scats = filterByScore(data.scats, minscore) || pcats; - const cattax = data.cattax ? data.cattax : CATTAX_IAB; + const cattax = (data.cattax || data.cattax === undefined) ? data.cattax : CATTAX_IAB; relevadData.categories = {cat: cats, pagecat: pcats, sectioncat: scats, cattax: cattax}; const contsegs = filterByScore(data.contsegs, minscore); @@ -268,15 +270,12 @@ export function addRtdData(reqBids, data, moduleConfig) { } if (wb && !isEmpty(relevadList)) { setBidderSiteAndContent(reqBids.ortb2Fragments?.bidder, bid.bidder, relevadData); + setBidderSiteAndContent(bid, 'ortb2', relevadData); deepSetValue(bid, 'params.keywords.relevad_rtd', relevadList); - deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); + !(bid.params?.target || '').includes('relevad_rtd=') && deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); let firstPartyData = {}; firstPartyData[bid.bidder] = { firstPartyData: { relevad_rtd: relevadList } }; config.setConfig(firstPartyData); - !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.segments', relevadData.segments); - !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.user.ext.data.contextual_categories', relevadData.categories.pagecat); - !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.site.ext.data.relevad_rtd', relevadData.categories.pagecat); - !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.relevad_rtd', relevadData.segments); } } } catch (e) { diff --git a/modules/relevantdigitalBidAdapter.js b/modules/relevantdigitalBidAdapter.js index ad9ee5e1e14..8d1265075f9 100644 --- a/modules/relevantdigitalBidAdapter.js +++ b/modules/relevantdigitalBidAdapter.js @@ -94,11 +94,18 @@ export const spec = { gvlid: 1100, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue **/ + /** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue */ isBidRequestValid: (bid) => bid.params?.placementId && getBidderConfig([bid]).complete, /** Trigger impression-pixel */ - onBidWon: ({pbsWurl}) => pbsWurl && triggerPixel(pbsWurl), + onBidWon(bid) { + if (bid.pbsWurl) { + triggerPixel(bid.pbsWurl) + } + if (bid.burl) { + triggerPixel(bid.burl) + } + }, /** Build BidRequest for PBS */ buildRequests(bidRequests, bidderRequest) { @@ -193,6 +200,24 @@ export const spec = { }); return syncs; }, + + /** If server side, transform bid params if needed */ + transformBidParams(params, isOrtb, adUnit, bidRequests) { + if (!params.placementId) { + return; + } + const bid = bidRequests.flatMap(req => req.adUnitsS2SCopy || []).flatMap((adUnit) => adUnit.bids).find((bid) => bid.params?.placementId === params.placementId); + if (!bid) { + return; + } + const cfg = getBidderConfig([bid]); + FIELDS.forEach(({ name }) => { + if (cfg[name] && !params[name]) { + params[name] = cfg[name]; + } + }); + return params; + }, }; registerBidder(spec); diff --git a/modules/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index 8264e0cc9cc..1fe4b4d750c 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -1,25 +1,29 @@ import { timestamp, deepAccess, isStr, deepClone } from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import { config } from '../src/config.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'; const BIDDER_CODE = 'resetdigital'; const CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [ 'banner', 'video' ], - isBidRequestValid: function(bid) { - return (!!(bid.params.pubId || bid.params.zoneId)); + supportedMediaTypes: ['banner', 'video'], + isBidRequestValid: function (bid) { + return !!(bid.params.pubId || bid.params.zoneId); }, - buildRequests: function(validBidRequests, bidderRequest) { - let stack = (bidderRequest.refererInfo && - bidderRequest.refererInfo.stack ? bidderRequest.refererInfo.stack - : []) - - let spb = (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : 5 + buildRequests: function (validBidRequests, bidderRequest) { + let stack = + bidderRequest.refererInfo && bidderRequest.refererInfo.stack + ? bidderRequest.refererInfo.stack + : []; + + let spb = + config.getConfig('userSync') && + config.getConfig('userSync').syncsPerBidder + ? config.getConfig('userSync').syncsPerBidder + : 5; const payload = { start_time: timestamp(), @@ -29,19 +33,19 @@ export const spec = { iframe: !bidderRequest.refererInfo.reachedTop, // TODO: the last element in refererInfo.stack is window.location.href, that's unlikely to have been the intent here url: stack && stack.length > 0 ? [stack.length - 1] : null, - https: (window.location.protocol === 'https:'), + https: window.location.protocol === 'https:', // TODO: is 'page' the right value here? - referrer: bidderRequest.refererInfo.page + referrer: bidderRequest.refererInfo.page, }, imps: [], user_ids: validBidRequests[0].userId, - sync_limit: spb + sync_limit: spb, }; if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = { applies: bidderRequest.gdprConsent.gdprApplies, - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, }; } @@ -50,10 +54,16 @@ export const spec = { } function getOrtb2Keywords(ortb2Obj) { - const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords']; + const fields = [ + 'site.keywords', + 'site.content.keywords', + 'user.keywords', + 'app.keywords', + 'app.content.keywords', + ]; let result = []; - fields.forEach(path => { + fields.forEach((path) => { let keyStr = deepAccess(ortb2Obj, path); if (isStr(keyStr)) result.push(keyStr); }); @@ -79,18 +89,26 @@ export const spec = { const floorInfo = req.getFloor({ currency: CURRENCY, mediaType: BANNER, - size: '*' + size: '*', }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + if ( + typeof floorInfo === 'object' && + floorInfo.currency === CURRENCY && + !isNaN(parseFloat(floorInfo.floor)) + ) { bidFloor = parseFloat(floorInfo.floor); bidFloorCur = CURRENCY; } } // get param kewords (if it exists) - let paramsKeywords = req.params.keywords ? req.params.keywords.split(',') : []; + let paramsKeywords = req.params.keywords + ? req.params.keywords.split(',') + : []; // merge all keywords - let keywords = ortb2KeywordsList.concat(paramsKeywords).concat(metaKeywords); + let keywords = ortb2KeywordsList + .concat(paramsKeywords) + .concat(metaKeywords); payload.imps.push({ pub_id: req.params.pubId, @@ -110,32 +128,32 @@ export const spec = { sizes: req.sizes, force_bid: req.params.forceBid, coppa: config.getConfig('coppa') === true ? 1 : 0, - media_types: deepAccess(req, 'mediaTypes') + media_types: deepAccess(req, 'mediaTypes'), }); } - let params = validBidRequests[0].params - let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com' + let params = validBidRequests[0].params; + let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com'; return { method: 'POST', url: url, data: JSON.stringify(payload), - bids: validBidRequests + bids: validBidRequests, }; }, - interpretResponse: function(serverResponse, bidRequest) { + interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; if (!serverResponse || !serverResponse.body) { - return bidResponses + return bidResponses; } let res = serverResponse.body; if (!res.bids || !res.bids.length) { - return [] + return []; } for (let x = 0; x < serverResponse.body.bids.length; x++) { - let bid = serverResponse.body.bids[x] + let bid = serverResponse.body.bids[x]; bidResponses.push({ requestId: bid.bid_id, @@ -152,47 +170,45 @@ export const spec = { netRevenue: true, currency: 'USD', meta: { - advertiserDomains: bid.adomain - } - }) + advertiserDomains: bid.adomain, + }, + }); } return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { - const syncs = [] - + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + let syncs = []; if (!serverResponses.length || !serverResponses[0].body) { - return syncs - } - - let pixels = serverResponses[0].body.pixels - if (!pixels || !pixels.length) { - return syncs + return syncs; } - let gdprParams = null + let gdprParams = ''; if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`; } else { - gdprParams = `gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; } } - for (let x = 0; x < pixels.length; x++) { - let pixel = pixels[x] - - if ((pixel.type === 'iframe' && syncOptions.iframeEnabled) || - (pixel.type === 'image' && syncOptions.pixelEnabled)) { - if (gdprParams && gdprParams.length) { - pixel = (pixel.indexOf('?') === -1 ? '?' : '&') + gdprParams - } - syncs.push(pixel) - } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://async.resetdigital.co/async_usersync.html?${gdprParams}`, + }); + } else if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://meta.resetdigital.co/pchain${ + gdprParams ? `?${gdprParams}` : '' + }`, + }); } return syncs; - } + }, }; registerBidder(spec); diff --git a/modules/retailspotBidAdapter.js b/modules/retailspotBidAdapter.js index 616b638e840..557dd617274 100644 --- a/modules/retailspotBidAdapter.js +++ b/modules/retailspotBidAdapter.js @@ -2,6 +2,11 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'retailspot'; const DEFAULT_SUBDOMAIN = 'ssp'; const PREPROD_SUBDOMAIN = 'ssp-preprod'; @@ -28,7 +33,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {bidRequests} - bidRequests.bids[] is an array of AdUnits and bids + * @param {BidRequests} - bidRequests.bids[] is an array of AdUnits and bids * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { diff --git a/modules/revcontentBidAdapter.js b/modules/revcontentBidAdapter.js index 5bf7dd691e7..f1d5521f780 100644 --- a/modules/revcontentBidAdapter.js +++ b/modules/revcontentBidAdapter.js @@ -3,9 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import {_map, deepAccess, getAdUnitSizes, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; +import {_map, deepAccess, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; import {parseDomain} from '../src/refererDetection.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'revcontent'; const NATIVE_PARAMS = { diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 1625912ddb8..b63e31266fb 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isStr} from '../src/utils.js'; +import {deepAccess, isStr, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; @@ -49,7 +49,7 @@ export const spec = { referer: (typeof bidderRequest.refererInfo.page != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.page) : null), numIframes: (typeof bidderRequest.refererInfo.numIframes != 'undefined' ? bidderRequest.refererInfo.numIframes : null), transactionId: bid.ortb2Imp?.ext?.tid, - timeout: config.getConfig('bidderTimeout'), + timeout: bidderRequest.timeout || 600, user: raiSetEids(bid), demand: raiGetDemandType(bid), videoData: raiGetVideoInfo(bid), @@ -75,6 +75,18 @@ export const spec = { } } + if (bidderRequest?.gppConsent) { + payload.privacy = { + gpp: bidderRequest.gppConsent.gppString, + gpp_sid: bidderRequest.gppConsent.applicableSections + } + } else if (bidderRequest?.ortb2?.regs?.gpp) { + payload.privacy = { + gpp: bidderRequest.ortb2.regs.gpp, + gpp_sid: bidderRequest.ortb2.regs.gpp_sid + } + } + var payloadString = JSON.stringify(payload); var endpoint = 'https://shb.richaudience.com/hb/'; @@ -145,12 +157,13 @@ export const spec = { * @param {gdprConsent} GPDR consent object * @returns {Array} */ - getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncs = []; var rand = Math.floor(Math.random() * 9999999999); var syncUrl = ''; var consent = ''; + var consentGPP = ''; var raiSync = {}; @@ -160,11 +173,20 @@ export const spec = { consent = `consentString=${gdprConsent.consentString}` } + // GPP Consent + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + consentGPP = 'gpp=' + encodeURIComponent(gppConsent.gppString); + consentGPP += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + if (syncOptions.iframeEnabled && raiSync.raiIframe != 'exclude') { syncUrl = 'https://sync.richaudience.com/dcf3528a0b8aa83634892d50e91c306e/?ord=' + rand if (consent != '') { syncUrl += `&${consent}` } + if (consentGPP != '') { + syncUrl += `&${consentGPP}` + } syncs.push({ type: 'iframe', url: syncUrl @@ -176,6 +198,9 @@ export const spec = { if (consent != '') { syncUrl += `&${consent}` } + if (consentGPP != '') { + syncUrl += `&${consentGPP}` + } syncs.push({ type: 'image', url: syncUrl @@ -183,6 +208,13 @@ export const spec = { } return syncs }, + + onTimeout: function (data) { + let url = raiGetTimeoutURL(data); + if (url) { + triggerPixel(url); + } + } }; registerBidder(spec); @@ -332,3 +364,15 @@ function raiGetFloor(bid, config) { return 0 } } + +function raiGetTimeoutURL(data) { + let {params, timeout} = data[0] + let url = 'https://s.richaudience.com/err/?ec=6&ev=[timeout_publisher]&pla=[placement_hash]&int=PREBID&pltfm=&node=&dm=[domain]'; + + url = url.replace('[timeout_publisher]', timeout) + url = url.replace('[placement_hash]', params[0].pid) + if (document.location.host != null) { + url = url.replace('[domain]', document.location.host) + } + return url +} diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index d5c6469db12..82790805303 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -7,7 +19,8 @@ const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'rise'; const ADAPTER_VERSION = '6.0.0'; const TTL = 360; -const CURRENCY = 'USD'; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_GVLID = 1043; const DEFAULT_SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; const MODES = { PRODUCTION: 'hb-multi', @@ -20,7 +33,11 @@ const SUPPORTED_SYNC_METHODS = { export const spec = { code: BIDDER_CODE, - gvlid: 1043, + aliases: [ + { code: 'risexchange', gvlid: DEFAULT_GVLID }, + { code: 'openwebxchange', gvlid: 280 } + ], + gvlid: DEFAULT_GVLID, version: ADAPTER_VERSION, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { @@ -61,7 +78,7 @@ export const spec = { const bidResponse = { requestId: adUnit.requestId, cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, + currency: adUnit.currency || DEFAULT_CURRENCY, width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, @@ -128,18 +145,20 @@ registerBidder(spec); /** * Get floor price * @param bid {bid} + * @param mediaType {string} + * @param currency {string} * @returns {Number} */ -function getFloor(bid, mediaType) { +function getFloor(bid, mediaType, currency) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ - currency: CURRENCY, + currency: currency, mediaType: mediaType, size: '*' }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; } /** @@ -277,7 +296,7 @@ function generateBidParameters(bid, bidderRequest) { const {params} = bid; const mediaType = isBanner(bid) ? BANNER : VIDEO; const sizesArray = getSizesArray(bid, mediaType); - + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; // fix floor price in case of NAN if (isNaN(params.floorPrice)) { params.floorPrice = 0; @@ -287,12 +306,13 @@ function generateBidParameters(bid, bidderRequest) { mediaType, adUnitCode: getBidIdParameter('adUnitCode', bid), sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), bidId: getBidIdParameter('bidId', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), transactionId: bid.ortb2Imp?.ext?.tid, - coppa: 0 + coppa: 0, }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); @@ -450,6 +470,14 @@ function generateGeneralParams(generalObject, bidderRequest) { generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; } + if (bidderRequest && bidderRequest.gppConsent) { + generalParams.gpp = bidderRequest.gppConsent.gppString; + generalParams.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + generalParams.gpp = bidderRequest.ortb2.regs.gpp; + generalParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + if (generalBidParams.ifa) { generalParams.ifa = generalBidParams.ifa; } diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index f0837cb5508..ac0ea559c88 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -26,6 +26,7 @@ The adapter supports Video(instream). | `testMode` | optional | Boolean | This activates the test mode | false | `rtbDomain` | optional | String | Sets the seller end point | "www.test.com" | `is_wrapper` | private | Boolean | Please don't use unless your account manager asked you to | false +| `currency` | optional | String | 3 letters currency | "EUR" # Test Parameters diff --git a/modules/rixengineBidAdapter.js b/modules/rixengineBidAdapter.js new file mode 100644 index 00000000000..8ffdb55f09b --- /dev/null +++ b/modules/rixengineBidAdapter.js @@ -0,0 +1,67 @@ +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'rixengine'; + +let ENDPOINT = null; +let SID = null; +let TOKEN = null; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY, + mediaType: BANNER, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + return imp; + }, +}); +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + if ( + Boolean(bid.params.endpoint) && + Boolean(bid.params.sid) && + Boolean(bid.params.token) + ) { + SID = bid.params.sid; + TOKEN = bid.params.token; + ENDPOINT = bid.params.endpoint + '?sid=' + SID + '&token=' + TOKEN; + return true; + } + return false; + }, + + buildRequests(bidRequests, bidderRequest) { + let data = converter.toORTB({ bidRequests, bidderRequest }); + + return [ + { + method: 'POST', + url: ENDPOINT, + data, + options: { contentType: 'application/json;charset=utf-8' }, + }, + ]; + }, + + interpretResponse(response, request) { + const bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids; + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/rixengineBidAdapter.md b/modules/rixengineBidAdapter.md new file mode 100644 index 00000000000..c05648f4b85 --- /dev/null +++ b/modules/rixengineBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: RixEngine Bid Adapter +Module Type: Bidder Adapter +Maintainer: yuanchang@algorix.co +``` + +# Description + +Connects to RixEngine exchange for bids. + +RixEngine bid adapter supports Banner currently. + +# Sample Banner Ad Unit: For Publishers +``` +var adUnits = [ +{ + sizes: [ + [320, 50] + ], + bids: [{ + bidder: 'rixengine', + params: { + endpoint: 'http://demo.svr.rixengine.com/rtb', // required + token: '1e05a767930d7d96ef6ce16318b4ab99', // required + sid: 36540, // required + } + }] +}]; +``` + diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 4ca4e4f90a9..1cd97696770 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isArray, logError, logInfo, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepClone, isArray, logError, logInfo, mergeDeep, isEmpty, isPlainObject, isNumber, isStr} from '../src/utils.js'; import {getOrigin} from '../libraries/getOrigin/index.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -18,6 +18,12 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; const GVLID = 16; +const DSA_ATTRIBUTES = [ + { name: 'dsarequired', 'min': 0, 'max': 3 }, + { name: 'pubrender', 'min': 0, 'max': 2 }, + { name: 'datatopub', 'min': 0, 'max': 2 } +]; + // Codes defined by OpenRTB Native Ads 1.1 specification export const OPENRTB = { NATIVE: { @@ -95,6 +101,17 @@ export const spec = { } }); + const dsa = deepAccess(ortb2Params, 'regs.ext.dsa'); + if (validateDSA(dsa)) { + mergeDeep(request, { + regs: { + ext: { + dsa + } + } + }); + } + let computedEndpointUrl = ENDPOINT_URL; if (bidderRequest.fledgeEnabled) { @@ -133,7 +150,13 @@ export const spec = { } else { interpretedBid = interpretBannerBid(serverBid); } - if (serverBid.ext) interpretedBid.ext = serverBid.ext; + + if (serverBid.ext) { + interpretedBid.ext = deepClone(serverBid.ext); + if (serverBid.ext.dsa) { + interpretedBid.meta = Object.assign({}, interpretedBid.meta, { dsa: serverBid.ext.dsa }); + } + } bids.push(interpretedBid); }); @@ -142,6 +165,7 @@ export const spec = { interpretResponse: function (serverResponse, originalRequest) { let bids; + const fledgeInterestGroupBuyers = config.getConfig('fledgeConfig.interestGroupBuyers') || []; const responseBody = serverResponse.body; let fledgeAuctionConfigs = null; @@ -163,7 +187,7 @@ export const spec = { { seller, decisionLogicUrl, - interestGroupBuyers: Object.keys(perBuyerSignals), + interestGroupBuyers: [...fledgeInterestGroupBuyers, ...Object.keys(perBuyerSignals)], perBuyerSignals, }, sellerTimeout @@ -195,7 +219,7 @@ registerBidder(spec); /** * @param {object} slot Ad Unit Params by Prebid - * @returns {int} floor by imp type + * @returns {number} floor by imp type */ function applyFloor(slot) { const floors = []; @@ -350,7 +374,7 @@ function mapNative(slot) { /** * @param {object} slot Slot config by Prebid - * @returns {array} Request Assets by OpenRTB Native Ads 1.1 §4.2 + * @returns {Array} Request Assets by OpenRTB Native Ads 1.1 §4.2 */ function mapNativeAssets(slot) { const params = slot.nativeParams || deepAccess(slot, 'mediaTypes.native'); @@ -413,7 +437,7 @@ function mapNativeAssets(slot) { /** * @param {object} image Prebid native.image/icon - * @param {int} type Image or icon code + * @param {number} type Image or icon code * @returns {object} Request Image by OpenRTB Native Ads 1.1 §4.4 */ function mapNativeImage(image, type) { @@ -518,3 +542,27 @@ function interpretNativeAd(adm) { }); return result; } + +/** + * https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + * + * @param {object} dsa + * @returns {boolean} whether dsa object contains valid attributes values + */ +function validateDSA(dsa) { + if (isEmpty(dsa) || !isPlainObject(dsa)) return false; + + return DSA_ATTRIBUTES.reduce((prev, attr) => { + const dsaEntry = dsa[attr.name]; + return prev && ( + !dsa.hasOwnProperty(attr.name) || + (isNumber(dsaEntry) && dsaEntry >= attr.min && dsaEntry <= attr.max) + ) + }, true) && + (!dsa.hasOwnProperty('transparency') || + (isArray(dsa.transparency) && dsa.transparency.every( + v => isPlainObject(v) && isStr(v.domain) && v.domain && isArray(v.dsaparams) && + v.dsaparams.every(x => isNumber(x)) + )) + ) +} diff --git a/modules/rtbsapeBidAdapter.js b/modules/rtbsapeBidAdapter.js index 5b1a92b02a0..502b62c8799 100644 --- a/modules/rtbsapeBidAdapter.js +++ b/modules/rtbsapeBidAdapter.js @@ -4,6 +4,14 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {OUTSTREAM} from '../src/video.js'; import {Renderer} from '../src/Renderer.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'rtbsape'; const ENDPOINT = 'https://ssp-rtb.sape.ru/prebid'; const RENDERER_SRC = 'https://cdn-rtb.sape.ru/js/player.js'; diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 633c4f4cdc1..c5308c91e18 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -1,6 +1,7 @@ /** * This module adds Real time data support to prebid.js * @module modules/realTimeData + * @typedef {import('../../modules/rtdModule/index.js').SubmoduleConfig} SubmoduleConfig */ /** @@ -30,7 +31,7 @@ */ /** - * @function? + * @function * @summary return real time data * @name RtdSubmodule#getTargetingData * @param {string[]} adUnitsCodes @@ -40,7 +41,7 @@ */ /** - * @function? + * @function * @summary modify bid request data * @name RtdSubmodule#getBidRequestData * @param {Object} reqBidsConfigObj @@ -73,7 +74,7 @@ */ /** - * @function? + * @function * @summary on auction init event * @name RtdSubmodule#onAuctionInitEvent * @param {Object} data @@ -82,7 +83,7 @@ */ /** - * @function? + * @function * @summary on auction end event * @name RtdSubmodule#onAuctionEndEvent * @param {Object} data @@ -91,7 +92,7 @@ */ /** - * @function? + * @function * @summary on bid response event * @name RtdSubmodule#onBidResponseEvent * @param {Object} data @@ -100,7 +101,7 @@ */ /** - * @function? + * @function * @summary on bid requested event * @name RtdSubmodule#onBidRequestEvent * @param {Object} data @@ -109,7 +110,7 @@ */ /** - * @function? + * @function * @summary on data deletion request * @name RtdSubmodule#onDataDeletionRequest * @param {SubmoduleConfig} config @@ -215,7 +216,8 @@ const setEventsListeners = (function () { [CONSTANTS.EVENTS.AUCTION_INIT]: ['onAuctionInitEvent'], [CONSTANTS.EVENTS.AUCTION_END]: ['onAuctionEndEvent', getAdUnitTargeting], [CONSTANTS.EVENTS.BID_RESPONSE]: ['onBidResponseEvent'], - [CONSTANTS.EVENTS.BID_REQUESTED]: ['onBidRequestEvent'] + [CONSTANTS.EVENTS.BID_REQUESTED]: ['onBidRequestEvent'], + [CONSTANTS.EVENTS.BID_ACCEPTED]: ['onBidAcceptedEvent'] }).forEach(([ev, [handler, preprocess]]) => { events.on(ev, (args) => { preprocess && preprocess(args); @@ -385,7 +387,7 @@ export function getAdUnitTargeting(auction) { /** * deep merge array of objects - * @param {array} arr - objects array + * @param {Array} arr - objects array * @return {Object} merged object */ export function deepMerge(arr) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index afabdaca637..c03065cd5a5 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -7,7 +7,6 @@ import { find } from '../src/polyfill.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { Renderer } from '../src/Renderer.js'; import { - convertTypes, deepAccess, deepSetValue, formatQS, @@ -18,16 +17,22 @@ import { logMessage, logWarn, mergeDeep, - parseSizesInput, _each + parseSizesInput, + pick, + _each } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.2.1.js'; // renderer code at https://github.com/rubicon-project/apex2 -let rubiConf = {}; +let rubiConf = config.getConfig('rubicon') || {}; // 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 => { @@ -124,6 +129,7 @@ var sizeMap = { 278: '320x500', 282: '320x400', 288: '640x380', + 484: '720x1280', 524: '1x2', 548: '500x1000', 550: '980x480', @@ -139,7 +145,9 @@ var sizeMap = { 576: '610x877', 578: '980x552', 580: '505x656', - 622: '192x160' + 622: '192x160', + 632: '1200x450', + 634: '340x450' }; _each(sizeMap, (item, key) => sizeMap[item] = key); @@ -193,13 +201,14 @@ export const converter = ortbConverter({ const imp = buildImp(bidRequest, context); imp.id = bidRequest.adUnitCode; delete imp.banner; - if (config.getConfig('s2sConfig.defaultTtl')) { - imp.exp = config.getConfig('s2sConfig.defaultTtl'); - }; - bidRequest.params.position === 'atf' && (imp.video.pos = 1); - bidRequest.params.position === 'btf' && (imp.video.pos = 3); + bidRequest.params.position === 'atf' && imp.video && (imp.video.pos = 1); + bidRequest.params.position === 'btf' && imp.video && (imp.video.pos = 3); delete imp.ext?.prebid?.storedrequest; + if (bidRequest.params.bidonmultiformat === true && bidRequestType.length > 1) { + deepSetValue(imp, 'ext.prebid.bidder.rubicon.formats', bidRequestType); + } + setBidFloors(bidRequest, imp); return imp; @@ -225,7 +234,7 @@ export const converter = ortbConverter({ }, context: { netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true - ttl: 300, + ttl: 360, }, processors: pbsExtensions }); @@ -318,7 +327,7 @@ export const spec = { ) ); }); - if (config.getConfig('rubicon.singleRequest') !== true) { + if (rubiConf.singleRequest !== true) { // bids are not grouped if single request mode is not enabled requests = filteredHttpRequest.concat(bannerBidRequests.map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); @@ -401,6 +410,8 @@ export const spec = { 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', + 'o_ae', + 'o_cdep', 'rp_floor', 'rp_secure', 'tk_user_key' @@ -474,6 +485,7 @@ export const spec = { 'x_source.tid': bidderRequest.ortb2?.source?.tid, 'x_imp.ext.tid': bidRequest.ortb2Imp?.ext?.tid, 'l_pb_bid_id': bidRequest.bidId, + 'o_cdep': bidRequest.ortb2?.device?.ext?.cdep, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), @@ -497,6 +509,11 @@ export const spec = { data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } + // Send multiformat data if requested + if (params.bidonmultiformat === true && deepAccess(bidRequest, 'mediaTypes') && Object.keys(bidRequest.mediaTypes).length > 1) { + data['p_formats'] = Object.keys(bidRequest.mediaTypes).join(','); + } + // add p_pos only if specified and valid // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value let posMapping = {1: 'atf', 3: 'btf'}; @@ -508,6 +525,12 @@ export const spec = { if (configUserId) { data['ppuid'] = configUserId; } + + if (bidRequest?.ortb2Imp?.ext?.ae) { + data['o_ae'] = 1; + } + + addDesiredSegtaxes(bidderRequest, data); // loop through userIds and add to request if (bidRequest.userIdAsEids) { bidRequest.userIdAsEids.forEach(eid => { @@ -528,7 +551,9 @@ export const spec = { data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; } else { // add anything else with this generic format - data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; + // if rubicon drop ^ + const id = eid.source === 'rubiconproject.com' ? eid.uids[0].id : `${eid.uids[0].id}^${eid.uids[0].atype || ''}` + data[`eid_${eid.source}`] = id; } // send AE "ppuid" signal if exists, and hasn't already been sent if (!data['ppuid']) { @@ -605,7 +630,7 @@ export const spec = { * @param {*} responseObj * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object - * @return {Bid[]} An array of bids which + * @return {{fledgeAuctionConfigs: *, bids: *}} An array of bids which */ interpretResponse: function (responseObj, request) { responseObj = responseObj.body; @@ -615,7 +640,6 @@ export const spec = { if (!responseObj || typeof responseObj !== 'object') { return []; } - // Response from PBS Java openRTB if (responseObj.seatbid) { const responseErrors = deepAccess(responseObj, 'ext.errors.rubicon'); @@ -641,7 +665,7 @@ export const spec = { return []; } - return ads.reduce((bids, ad, i) => { + let bids = ads.reduce((bids, ad, i) => { (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; if (ad.status !== 'ok') { @@ -658,7 +682,7 @@ export const spec = { creativeId: ad.creative_id || `${ad.network || ''}-${ad.advertiser || ''}`, cpm: ad.cpm || 0, dealId: ad.deal, - ttl: 300, // 5 minutes + ttl: 360, // 6 minutes netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true rubicon: { advertiserId: ad.advertiser, networkId: ad.network @@ -672,6 +696,10 @@ export const spec = { bid.mediaType = ad.creative_type; } + if (ad.dsa && Object.keys(ad.dsa).length) { + bid.meta.dsa = ad.dsa; + } + if (ad.adomain) { bid.meta.advertiserDomains = Array.isArray(ad.adomain) ? ad.adomain : [ad.adomain]; } @@ -702,6 +730,16 @@ export const spec = { }, []).sort((adA, adB) => { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); + + let fledgeAuctionConfigs = responseObj.component_auction_config?.map(config => { + return { config, bidId: config.bidId } + }); + + if (fledgeAuctionConfigs) { + return { bids, fledgeAuctionConfigs }; + } else { + return bids; + } }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (!hasSynced && syncOptions.iframeEnabled) { @@ -734,19 +772,6 @@ export const spec = { url: `https://${rubiConf.syncHost || 'eus'}.rubiconproject.com/usync.html` + params }; } - }, - /** - * Covert bid param types for S2S - * @param {Object} params bid params - * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol - * @return {Object} params bid params - */ - transformBidParams: function(params, isOpenRtb) { - return convertTypes({ - 'accountId': 'number', - 'siteId': 'number', - 'zoneId': 'number' - }, params); } }; @@ -803,7 +828,14 @@ function renderBid(bid) { hideSmartAdServerIframe(adUnitElement); // configure renderer - const config = bid.renderer.getConfig(); + const defaultConfig = { + align: 'center', + position: 'append', + closeButton: false, + label: undefined, + collapse: true + }; + const config = { ...defaultConfig, ...bid.renderer.getConfig() }; bid.renderer.push(() => { window.MagniteApex.renderAd({ width: bid.width, @@ -811,12 +843,12 @@ function renderBid(bid) { vastUrl: bid.vastUrl, placement: { attachTo: adUnitElement, - align: config.align || 'center', - position: config.position || 'append' + align: config.align, + position: config.position }, - closeButton: config.closeButton || false, - label: config.label || undefined, - collapse: config.collapse || true + closeButton: config.closeButton, + label: config.label, + collapse: config.collapse }); }); } @@ -884,6 +916,7 @@ function applyFPD(bidRequest, mediaType, data) { let impExtData = deepAccess(bidRequest.ortb2Imp, 'ext.data') || {}; const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + const dsa = deepAccess(fpd, 'regs.ext.dsa'); const SEGTAX = {user: [4], site: [1, 2, 5, 6]}; const MAP = {user: 'tg_v.', site: 'tg_i.', adserver: 'tg_i.dfp_ad_unit_code', pbadslot: 'tg_i.pbadslot', keywords: 'kw'}; const validate = function(prop, key, parentName) { @@ -939,10 +972,57 @@ function applyFPD(bidRequest, mediaType, data) { data['p_gpid'] = gpid; } + // add dsa signals + if (dsa && Object.keys(dsa).length) { + pick(dsa, [ + 'dsainfo', (dsainfo) => data['dsainfo'] = dsainfo, + 'dsarequired', (required) => data['dsarequired'] = required, + 'pubrender', (pubrender) => data['dsapubrender'] = pubrender, + 'datatopub', (datatopub) => data['dsadatatopubs'] = datatopub, + 'transparency', (transparency) => { + if (Array.isArray(transparency) && transparency.length) { + data['dsatransparency'] = transparency.reduce((param, transp) => { + if (param) { + param += '~~' + } + return param += `${transp.domain}~${transp.dsaparams.join('_')}` + }, '') + } + } + ]) + } + // only send one of pbadslot or dfp adunit code (prefer pbadslot) if (data['tg_i.pbadslot']) { delete data['tg_i.dfp_ad_unit_code']; } + + // High Entropy stuff -> sua object is the ORTB standard (default to pass unless specifically disabled) + const clientHints = deepAccess(fpd, 'device.sua'); + if (clientHints && rubiConf.chEnabled !== false) { + // pick out client hints we want to send (any that are undefined or empty will NOT be sent) + pick(clientHints, [ + 'architecture', arch => data.m_ch_arch = arch, + 'bitness', bitness => data.m_ch_bitness = bitness, + 'browsers', browsers => { + if (!Array.isArray(browsers)) return; + // reduce down into ua and full version list attributes + const [ua, fullVer] = browsers.reduce((accum, browserData) => { + accum[0].push(`"${browserData?.brand}"|v="${browserData?.version?.[0]}"`); + accum[1].push(`"${browserData?.brand}"|v="${browserData?.version?.join?.('.')}"`); + return accum; + }, [[], []]); + data.m_ch_ua = ua?.join?.(','); + data.m_ch_full_ver = fullVer?.join?.(','); + }, + 'mobile', isMobile => data.m_ch_mobile = `?${isMobile}`, + 'model', model => data.m_ch_model = model, + 'platform', platform => { + data.m_ch_platform = platform?.brand; + data.m_ch_platform_ver = platform?.version?.join?.('.'); + } + ]) + } } else { if (Object.keys(impExt).length) { mergeDeep(data.imp[0].ext, impExt); @@ -956,6 +1036,27 @@ function applyFPD(bidRequest, mediaType, data) { } } +function addDesiredSegtaxes(bidderRequest, target) { + if (rubiConf.readTopics === false) { + return; + } + let iSegments = [1, 2, 5, 6, 7, 507].concat(rubiConf.sendSiteSegtax?.map(seg => Number(seg)) || []); + let vSegments = [4, 508].concat(rubiConf.sendUserSegtax?.map(seg => Number(seg)) || []); + let userData = bidderRequest.ortb2?.user?.data || []; + let siteData = bidderRequest.ortb2?.site?.content?.data || []; + userData.forEach(iterateOverSegmentData(target, 'v', vSegments)); + siteData.forEach(iterateOverSegmentData(target, 'i', iSegments)); +} + +function iterateOverSegmentData(target, char, segments) { + return (topic) => { + const taxonomy = Number(topic.ext?.segtax); + if (segments.includes(taxonomy)) { + target[`tg_${char}.tax${taxonomy}`] = topic.segment?.map(seg => seg.id).join(','); + } + } +} + /** * @param sizes * @returns {*} @@ -1124,8 +1225,7 @@ export function hasValidVideoParams(bid) { var requiredParams = { mimes: arrayType, protocols: arrayType, - linearity: numberType, - api: arrayType + linearity: numberType } // loop through each param and verify it has the correct Object.keys(requiredParams).forEach(function(param) { @@ -1140,7 +1240,7 @@ export function hasValidVideoParams(bid) { /** * Make sure the required params are present * @param {Object} schain - * @param {Bool} + * @param {boolean} */ export function hasValidSupplyChainParams(schain) { let isValid = false; diff --git a/modules/schain.js b/modules/schain.js index 2991bb5b3d5..726679b133f 100644 --- a/modules/schain.js +++ b/modules/schain.js @@ -1,16 +1,17 @@ -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import adapterManager from '../src/adapterManager.js'; import { - isNumber, - isStr, + _each, + deepAccess, + deepClone, + deepSetValue, isArray, + isInteger, + isNumber, isPlainObject, - hasOwn, + isStr, logError, - isInteger, - _each, - logWarn, - deepAccess, deepSetValue, deepClone + logWarn } from '../src/utils.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; @@ -63,7 +64,7 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // ext: Object [optional] - if (hasOwn(schainObject, 'ext')) { + if (schainObject.hasOwnProperty('ext')) { if (!isPlainObject(schainObject.ext)) { appendFailMsg(`schain.config.ext` + shouldBeAnObject); } @@ -92,28 +93,28 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // rid: String [Optional] - if (hasOwn(node, 'rid')) { + if (node.hasOwnProperty('rid')) { if (!isStr(node.rid)) { appendFailMsg(`schain.config.nodes[${index}].rid` + shouldBeAString); } } // name: String [Optional] - if (hasOwn(node, 'name')) { + if (node.hasOwnProperty('name')) { if (!isStr(node.name)) { appendFailMsg(`schain.config.nodes[${index}].name` + shouldBeAString); } } // domain: String [Optional] - if (hasOwn(node, 'domain')) { + if (node.hasOwnProperty('domain')) { if (!isStr(node.domain)) { appendFailMsg(`schain.config.nodes[${index}].domain` + shouldBeAString); } } // ext: Object [Optional] - if (hasOwn(node, 'ext')) { + if (node.hasOwnProperty('ext')) { if (!isPlainObject(node.ext)) { appendFailMsg(`schain.config.nodes[${index}].ext` + shouldBeAnObject); } diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index 7ac7d048c50..6f36c8a191e 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -3,6 +3,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'seedtag'; const SEEDTAG_ALIAS = 'st'; const SEEDTAG_SSP_ENDPOINT = 'https://s.seedtag.com/c/hb/bid'; @@ -107,7 +116,6 @@ function buildBidRequest(validBidRequest) { return mediaTypesMap[pbjsType]; } ); - const bidRequest = { id: validBidRequest.bidId, transactionId: validBidRequest.ortb2Imp?.ext?.tid, @@ -115,6 +123,7 @@ function buildBidRequest(validBidRequest) { supplyTypes: mediaTypes, adUnitId: params.adUnitId, adUnitCode: validBidRequest.adUnitCode, + geom: geom(validBidRequest.adUnitCode), placement: params.placement, requestCount: validBidRequest.bidderRequestsCount || 1, // FIXME : in unit test the parameter bidderRequestsCount is undefined }; @@ -198,6 +207,27 @@ function ttfb() { return ttfb >= 0 && ttfb <= performance.now() ? ttfb : 0; } +function geom(adunitCode) { + const slot = document.getElementById(adunitCode); + if (slot) { + const scrollY = window.scrollY; + const { top, left, width, height } = slot.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight, + }; + + return { + scrollY, + top, + left, + width, + height, + viewport, + }; + } +} + export function getTimeoutUrl(data) { let queryParams = ''; if ( diff --git a/modules/setupadBidAdapter.js b/modules/setupadBidAdapter.js new file mode 100644 index 00000000000..55677d51c56 --- /dev/null +++ b/modules/setupadBidAdapter.js @@ -0,0 +1,271 @@ +import { + _each, + createTrackPixelHtml, + deepAccess, + isStr, + getBidIdParameter, + triggerPixel, + logWarn, +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'setupad'; +const ENDPOINT = 'https://prebid.setupad.io/openrtb2/auction'; +const SYNC_ENDPOINT = 'https://cookie.stpd.cloud/sync?'; +const REPORT_ENDPOINT = 'https://adapter-analytics.setupad.io/api/adapter-analytics'; +const GVLID = 1241; +const TIME_TO_LIVE = 360; +const biddersCreativeIds = {}; + +function getEids(bidRequest) { + if (deepAccess(bidRequest, 'userIdAsEids')) return bidRequest.userIdAsEids; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + gvlid: GVLID, + + isBidRequestValid: function (bid) { + return !!(bid.params.placement_id && isStr(bid.params.placement_id)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const requests = []; + + _each(validBidRequests, function (bid) { + const id = getBidIdParameter('placement_id', bid.params); + const accountId = getBidIdParameter('account_id', bid.params); + const auctionId = bid.auctionId; + const bidId = bid.bidId; + const eids = getEids(bid) || undefined; + let sizes = bid.sizes; + if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; + + const site = { + page: bidderRequest?.refererInfo?.page, + ref: bidderRequest?.refererInfo?.ref, + domain: bidderRequest?.refererInfo?.domain, + }; + const device = { + w: bidderRequest?.ortb2?.device?.w, + h: bidderRequest?.ortb2?.device?.h, + }; + + const payload = { + id: bid?.bidderRequestId, + ext: { + prebid: { + storedrequest: { + id: accountId || 'default', + }, + }, + }, + user: { ext: { eids } }, + device, + site, + imp: [], + }; + + const imp = { + id: bid.adUnitCode, + ext: { + prebid: { + storedrequest: { id }, + }, + }, + }; + + if (deepAccess(bid, 'mediaTypes.banner')) { + imp.banner = { + format: (sizes || []).map((s) => { + return { w: s[0], h: s[1] }; + }), + }; + } + + payload.imp.push(imp); + + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const uspConsent = bidderRequest && bidderRequest.uspConsent; + + if (gdprConsent || uspConsent) { + payload.regs = { ext: {} }; + + if (uspConsent) payload.regs.ext.us_privacy = uspConsent; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + payload.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + } + + if (typeof gdprConsent.consentString !== 'undefined') { + payload.user.ext.consent = gdprConsent.consentString; + } + } + } + const params = bid.params; + + requests.push({ + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload), + options: { + contentType: 'text/plain', + withCredentials: true, + }, + + bidId, + params, + auctionId, + }); + }); + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + if ( + !serverResponse || + !serverResponse.body || + typeof serverResponse.body != 'object' || + Object.keys(serverResponse.body).length === 0 + ) { + logWarn('no response or body is malformed'); + return []; + } + + const serverBody = serverResponse.body; + const bidResponses = []; + + _each(serverBody.seatbid, (res) => { + _each(res.bid, (bid) => { + const requestId = bidRequest.bidId; + const params = bidRequest.params; + const { ad, adUrl } = getAd(bid); + + const bidResponse = { + requestId, + params, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.id, + currency: serverBody.cur, + netRevenue: true, + ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: bid.adomain || [], + }, + }; + + // set a seat for creativeId for triggerPixel url + biddersCreativeIds[bidResponse.creativeId] = res.seat; + + bidResponse.ad = ad; + bidResponse.adUrl = adUrl; + bidResponses.push(bidResponse); + }); + }); + + return bidResponses; + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!responses?.length) return []; + + const syncs = []; + const bidders = getBidders(responses); + + if (syncOptions.iframeEnabled && bidders) { + const queryParams = []; + + queryParams.push(`bidders=${bidders}`); + queryParams.push('gdpr=' + +gdprConsent.gdprApplies); + queryParams.push('gdpr_consent=' + gdprConsent.consentString); + queryParams.push('usp_consent=' + (uspConsent || '')); + + const strQueryParams = queryParams.join('&'); + + syncs.push({ + type: 'iframe', + url: SYNC_ENDPOINT + strQueryParams + '&type=iframe', + }); + + return syncs; + } + + return []; + }, + + onBidWon: function (bid) { + let bidder = bid.bidder || bid.bidderCode; + const auctionId = bid.auctionId; + if (bidder !== BIDDER_CODE) return; + + let params; + if (bid.params) { + params = Array.isArray(bid.params) ? bid.params : [bid.params]; + } else { + if (Array.isArray(bid.bids)) { + params = bid.bids.map((singleBid) => singleBid.params); + } + } + + if (!params?.length) return; + + const placementIdsArray = []; + params.forEach((param) => { + if (!param.placement_id) return; + placementIdsArray.push(param.placement_id); + }); + + const placementIds = (placementIdsArray.length && placementIdsArray.join(';')) || ''; + + if (!placementIds) return; + + let extraBidParams = ''; + + // find the winning bidder by using creativeId as identification + if (biddersCreativeIds.hasOwnProperty(bid.creativeId) && biddersCreativeIds[bid.creativeId]) { + bidder = biddersCreativeIds[bid.creativeId]; + } + + // Add extra parameters + extraBidParams = `&cpm=${bid.originalCpm}¤cy=${bid.originalCurrency}`; + + const url = `${REPORT_ENDPOINT}?event=bidWon&bidder=${bidder}&placementIds=${placementIds}&auctionId=${auctionId}${extraBidParams}×tamp=${Date.now()}`; + triggerPixel(url); + }, +}; + +function getBidders(serverResponse) { + const bidders = serverResponse + .map((res) => Object.keys(res.body.ext.responsetimemillis || [])) + .flat(1); + + if (bidders.length) { + return encodeURIComponent(JSON.stringify([...new Set(bidders)])); + } +} + +function getAd(bid) { + let ad, adUrl; + + switch (deepAccess(bid, 'ext.prebid.type')) { + default: + if (bid.adm && bid.nurl) { + ad = bid.adm; + ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } else if (bid.adm) { + ad = bid.adm; + } else if (bid.nurl) { + adUrl = bid.nurl; + } + } + + return { ad, adUrl }; +} + +registerBidder(spec); diff --git a/modules/setupadBidAdapter.md b/modules/setupadBidAdapter.md new file mode 100644 index 00000000000..0d4f0ef392e --- /dev/null +++ b/modules/setupadBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +```text +Module Name: Setupad Bid Adapter +Module Type: Bidder Adapter +Maintainer: it@setupad.com +``` + +# Description + +Module that connects to Setupad's demand sources. + +# Test Parameters + +```js +const adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'setupad', + params: { + placement_id: '123', //required + account_id: '123', //optional + }, + }, + ], + }, +]; +``` diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 9046d6a633d..fa8b5e3bfdb 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -13,6 +13,14 @@ import {VENDORLESS_GVLID} from '../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; import {domainOverrideToRootDomain} from '../libraries/domainOverrideToRootDomain/index.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'sharedId'}); const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; diff --git a/modules/sharethroughAnalyticsAdapter.js b/modules/sharethroughAnalyticsAdapter.js index 6502c7e3a53..dc621e8da92 100644 --- a/modules/sharethroughAnalyticsAdapter.js +++ b/modules/sharethroughAnalyticsAdapter.js @@ -1,6 +1,6 @@ -import { tryAppendQueryString } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index d3f4b456aeb..2264bc37ebb 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ -import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { deepAccess, generateUUID, inIframe, mergeDeep } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -18,14 +18,14 @@ export const sharethroughAdapterSpec = { code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], gvlid: 80, - isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, + isBidRequestValid: (bid) => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { const timeout = bidderRequest.timeout; const firstPartyData = bidderRequest.ortb2 || {}; const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; - const secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); + const secure = nonHttp || sharethroughInternal.getProtocol().indexOf('https') > -1; const req = { id: generateUUID(), @@ -45,6 +45,7 @@ export const sharethroughAdapterSpec = { dnt: navigator.doNotTrack === '1' ? 1 : 0, h: window.screen.height, w: window.screen.width, + ext: {}, }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, @@ -63,6 +64,10 @@ export const sharethroughAdapterSpec = { test: 0, }; + if (bidderRequest.ortb2?.device?.ext?.cdep) { + req.device.ext['cdep'] = bidderRequest.ortb2.device.ext.cdep; + } + req.user = nullish(firstPartyData.user, {}); if (!req.user.ext) req.user.ext = {}; req.user.ext.eids = bidRequests[0].userIdAsEids || []; @@ -79,64 +84,96 @@ export const sharethroughAdapterSpec = { req.regs.ext.us_privacy = bidderRequest.uspConsent; } - const imps = bidRequests.map(bidReq => { - const impression = { ext: {} }; + if (bidderRequest?.gppConsent?.gppString) { + req.regs.gpp = bidderRequest.gppConsent.gppString; + req.regs.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest?.ortb2?.regs?.gpp) { + req.regs.ext.gpp = bidderRequest.ortb2.regs.gpp; + req.regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + + if (bidderRequest?.ortb2?.regs?.ext?.dsa) { + req.regs.ext.dsa = bidderRequest.ortb2.regs.ext.dsa; + } - // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose - const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); - if (tid) impression.ext.tid = tid; - const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); - if (gpid) impression.ext.gpid = gpid; + const imps = bidRequests + .map((bidReq) => { + const impression = { ext: {} }; - const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); + // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose + const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); + if (tid) impression.ext.tid = tid; + const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); + if (gpid) impression.ext.gpid = gpid; - if (videoRequest) { - // default playerSize, only change this if we know width and height are properly defined in the request - let [w, h] = [640, 360]; - if (videoRequest.playerSize && videoRequest.playerSize[0] && videoRequest.playerSize[0][0] && videoRequest.playerSize[0][1]) { - [w, h] = videoRequest.playerSize[0]; + const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); + + if (bidderRequest.fledgeEnabled && bidReq.mediaTypes.banner) { + mergeDeep(impression, { ext: { ae: 1 } }); // ae = auction environment; if this is 1, ad server knows we have a fledge auction } - impression.video = { - pos: nullish(videoRequest.pos, 0), - topframe: inIframe() ? 0 : 1, - skip: nullish(videoRequest.skip, 0), - linearity: nullish(videoRequest.linearity, 1), - minduration: nullish(videoRequest.minduration, 5), - maxduration: nullish(videoRequest.maxduration, 60), - playbackmethod: videoRequest.playbackmethod || [2], - api: getVideoApi(videoRequest), - mimes: videoRequest.mimes || ['video/mp4'], - protocols: getVideoProtocols(videoRequest), - w, - h, - startdelay: nullish(videoRequest.startdelay, 0), - skipmin: nullish(videoRequest.skipmin, 0), - skipafter: nullish(videoRequest.skipafter, 0), - placement: videoRequest.context === 'instream' ? 1 : +deepAccess(videoRequest, 'placement', 4), - }; + if (videoRequest) { + // default playerSize, only change this if we know width and height are properly defined in the request + let [w, h] = [640, 360]; + if ( + videoRequest.playerSize && + videoRequest.playerSize[0] && + videoRequest.playerSize[0][0] && + videoRequest.playerSize[0][1] + ) { + [w, h] = videoRequest.playerSize[0]; + } + + const getVideoPlacementValue = (vidReq) => { + if (vidReq.plcmt) { + return vidReq.placement; + } else { + return vidReq.context === 'instream' ? 1 : +deepAccess(vidReq, 'placement', 4); + } + }; + + impression.video = { + pos: nullish(videoRequest.pos, 0), + topframe: inIframe() ? 0 : 1, + skip: nullish(videoRequest.skip, 0), + linearity: nullish(videoRequest.linearity, 1), + minduration: nullish(videoRequest.minduration, 5), + maxduration: nullish(videoRequest.maxduration, 60), + playbackmethod: videoRequest.playbackmethod || [2], + api: getVideoApi(videoRequest), + mimes: videoRequest.mimes || ['video/mp4'], + protocols: getVideoProtocols(videoRequest), + w, + h, + startdelay: nullish(videoRequest.startdelay, 0), + skipmin: nullish(videoRequest.skipmin, 0), + skipafter: nullish(videoRequest.skipafter, 0), + placement: getVideoPlacementValue(videoRequest), + plcmt: videoRequest.plcmt ? videoRequest.plcmt : null, + }; + + if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; + if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; + if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; + } else { + impression.banner = { + pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), + topframe: inIframe() ? 0 : 1, + format: bidReq.sizes.map((size) => ({ w: +size[0], h: +size[1] })), + }; + } - if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; - if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; - if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; - } else { - impression.banner = { - pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), - topframe: inIframe() ? 0 : 1, - format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })), + return { + id: bidReq.bidId, + tagid: String(bidReq.params.pkey), + secure: secure ? 1 : 0, + bidfloor: getBidRequestFloor(bidReq), + ...impression, }; - } + }) + .filter((imp) => !!imp); - return { - id: bidReq.bidId, - tagid: String(bidReq.params.pkey), - secure: secure ? 1 : 0, - bidfloor: getBidRequestFloor(bidReq), - ...impression, - }; - }).filter(imp => !!imp); - - return imps.map(impression => { + return imps.map((impression) => { return { method: 'POST', url: STR_ENDPOINT, @@ -149,11 +186,19 @@ export const sharethroughAdapterSpec = { }, interpretResponse: ({ body }, req) => { - if (!body || !body.seatbid || body.seatbid.length === 0 || !body.seatbid[0].bid || body.seatbid[0].bid.length === 0) { + if ( + !body || + !body.seatbid || + body.seatbid.length === 0 || + !body.seatbid[0].bid || + body.seatbid[0].bid.length === 0 + ) { return []; } - return body.seatbid[0].bid.map(bid => { + const fledgeAuctionEnabled = body.ext?.auctionConfigs; + + const bidsFromExchange = body.seatbid[0].bid.map((bid) => { // Spec: https://docs.prebid.org/dev-docs/bidder-adaptor.html#interpreting-the-response const response = { requestId: bid.impid, @@ -193,27 +238,32 @@ export const sharethroughAdapterSpec = { return response; }); + + if (fledgeAuctionEnabled) { + return { + bids: bidsFromExchange, + fledgeAuctionConfigs: body.ext?.auctionConfigs || {}, + }; + } else { + return bidsFromExchange; + } }, getUserSyncs: (syncOptions, serverResponses) => { - const shouldCookieSync = syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; + const shouldCookieSync = + syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; - return shouldCookieSync - ? serverResponses[0].body.cookieSyncUrls.map(url => ({ type: 'image', url: url })) - : []; + return shouldCookieSync ? serverResponses[0].body.cookieSyncUrls.map((url) => ({ type: 'image', url: url })) : []; }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => { - }, + onTimeout: (data) => {}, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => { - }, + onBidWon: (bid) => {}, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => { - }, + onSetTargeting: (bid) => {}, }; function getVideoApi({ api }) { @@ -240,7 +290,7 @@ function getBidRequestFloor(bid) { const floorInfo = bid.getFloor({ currency: 'USD', mediaType: bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner', - size: bid.sizes.map(size => ({ w: size[0], h: size[1] })), + size: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), }); if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); diff --git a/modules/shinezBidAdapter.js b/modules/shinezBidAdapter.js index 96b6d281fdc..47fca317de2 100644 --- a/modules/shinezBidAdapter.js +++ b/modules/shinezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; diff --git a/modules/shinezRtbBidAdapter.js b/modules/shinezRtbBidAdapter.js new file mode 100644 index 00000000000..d1d9f36a569 --- /dev/null +++ b/modules/shinezRtbBidAdapter.js @@ -0,0 +1,336 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'shinezRtb'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.sweetgum.io`; +} + +export function extractCID(params) { + return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; +} + +export function extractPID(params) { + return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; +} + +export function extractSubDomain(params) { + return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.sweetgum.io/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.sweetgum.io/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/shinezRtbBidAdapter.md b/modules/shinezRtbBidAdapter.md new file mode 100644 index 00000000000..e9190c2a9c4 --- /dev/null +++ b/modules/shinezRtbBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Shinez RTB Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** tech-team@shinez.io + +# Description + +Module that connects to Shinez RTB demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'shinezRtb', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index a241cb71a5d..bd2706a21d5 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -1,10 +1,9 @@ import { deepAccess, - getBidIdParameter, getWindowTop, triggerPixel, logInfo, - logError + logError, getBidIdParameter } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; @@ -29,8 +28,11 @@ function getEnvURLs(isStage) { } } +const GVLID = 111; + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['showheroesBs'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function(bid) { diff --git a/modules/silvermobBidAdapter.js b/modules/silvermobBidAdapter.js new file mode 100644 index 00000000000..340dc9c70ac --- /dev/null +++ b/modules/silvermobBidAdapter.js @@ -0,0 +1,76 @@ +// import { logMessage } from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; + +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'silvermob'; +const AD_URL = 'https://{HOST}.silvermob.com/marketplace/api/dsp/prebidjs/{ZONEID}'; +const GVLID = 1058; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.ext = { + [BIDDER_CODE]: { + zoneid: bidRequest.params.zoneid, + host: bidRequest.params.host || 'us', + } + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.test = config.getConfig('debug') ? 1 : 0; + if (!request.cur) request.cur = [bid.params.currency || 'USD']; + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || 'USD'; + return bidResponse; + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(bid.params.zoneid)); + }, + + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return []; + + const host = validBidRequests[0].params.host || 'us'; + const zoneid = validBidRequests[0].params.zoneid; + + const data = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); + + return { + method: 'POST', + url: AD_URL.replace('{HOST}', host).replace('{ZONEID}', zoneid), + data: data + }; + }, + + interpretResponse: (response, request) => { + if (response?.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; + } + +}; + +registerBidder(spec); diff --git a/modules/silvermobBidAdapter.md b/modules/silvermobBidAdapter.md new file mode 100644 index 00000000000..ba080ec105e --- /dev/null +++ b/modules/silvermobBidAdapter.md @@ -0,0 +1,70 @@ +# Overview + +``` +Module Name: SilverMob Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@silvermob.com +``` + +# Description + +Module that connects to SilverMob platform + +# Test Parameters +``` + var adUnits = [ + // Will return static native ad. Assets are stored through user UI for each placement separetly + { + code: 'placementId_0', + mediaTypes: { + native: {} + }, + bids: [ + { + bidder: 'silvermob', + params: { + host: 'us', + zoneid: '0' + } + } + ] + }, + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'silvermob', + params: { + host: 'us', + zoneid: '0' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'silvermob', + params: { + host: 'us', + zoneid: '0' + } + } + ] + } + ]; +``` diff --git a/modules/silverpushBidAdapter.js b/modules/silverpushBidAdapter.js index df81e144380..5403f3bd88c 100644 --- a/modules/silverpushBidAdapter.js +++ b/modules/silverpushBidAdapter.js @@ -12,7 +12,7 @@ const bidderConfig = 'sp_pb_ortb'; const bidderVersion = '1.0.0'; const DEFAULT_CURRENCY = 'USD'; -export const REQUEST_URL = 'https://apac.chocolateplatform.com/bidder/?identifier=prebidchoc'; +export const REQUEST_URL = 'https://prebid.chocolateplatform.co/bidder/?identifier=prebidchoc'; export const SP_OUTSTREAM_PLAYER_URL = 'https://xaido.sgp1.cdn.digitaloceanspaces.com/prebid/spoutstream.min.js'; const VIDEO_ORTB_PARAMS = [ @@ -110,7 +110,7 @@ export const CONVERTER = ortbConverter({ bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); } - bidResponse = buildVideoVastResponse(bidResponse) + bidResponse = buildVideoVastResponse(bidResponse); bidResponse = buildVideoOutstreamResponse(bidResponse, context) return bidResponse; diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js index aaa3c48856b..97d0ec5219c 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -30,6 +30,7 @@ const partnerIds = { 'appnexus': 27446, 'appnexusAst': 27446, 'brealtime': 27446, + 'emetriq': 27446, 'emxdigital': 27446, 'pagescience': 27446, 'gourmetads': 33394, @@ -353,6 +354,7 @@ export function addSegmentData(reqBids, data, moduleConfig, onDone) { case 'appnexus': case 'appnexusAst': case 'brealtime': + case 'emetriq': case 'emxdigital': case 'pagescience': case 'gourmetads': diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index d212d98f50b..5ddb2e410cb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -63,7 +63,7 @@ export function isUsingNewSizeMapping(adUnits) { does not recognize. @params {Array} adUnits @returns {Array} validateAdUnits - Unrecognized properties are deleted. -*/ + */ export function checkAdUnitSetupHook(adUnits) { const validateSizeConfig = function (mediaType, sizeConfig, adUnitCode) { let isValid = true; diff --git a/modules/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index 447e314958f..250c1ebb19e 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -1,10 +1,17 @@ -import { getValue, parseSizesInput, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue, parseSizesInput} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ajax } from '../src/ajax.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'slimcut'; const ENDPOINT_URL = 'https://sb.freeskreen.com/pbr'; export const spec = { @@ -13,11 +20,11 @@ export const spec = { aliases: [{ code: 'scm', gvlid: 102 }], supportedMediaTypes: ['video', '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. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { let isValid = false; if (typeof bid.params !== 'undefined' && !isNaN(parseInt(getValue(bid.params, 'placementId'))) && parseInt(getValue(bid.params, 'placementId')) > 0) { @@ -26,11 +33,11 @@ export const spec = { return isValid; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const bids = validBidRequests.map(buildRequestObject); const payload = { @@ -55,11 +62,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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, request) { const bidResponses = []; serverResponse = serverResponse.body; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 1b50e033074..ac0422842d5 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -1,22 +1,20 @@ -import { - chunk, - deepAccess, - deepSetValue, - fill, - getAdUnitSizes, - getDNT, - getMaxValueFromArray, - getMinValueFromArray, - isEmpty, - isNumber, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, getDNT, isEmpty, isNumber, logError, logInfo} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import CONSTANTS from '../src/constants.json'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {fill} from '../libraries/appnexusUtils/anUtils.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const { NATIVE_IMAGE_TYPES } = CONSTANTS; const BIDDER_CODE = 'smaato'; @@ -466,7 +464,7 @@ function createAdPodImp(bidRequest, videoMediaType) { }); } else { // all maxdurations should be the same - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); imps.map((imp, index) => { const sequence = index + 1; imp.video.maxduration = maxDuration @@ -481,7 +479,7 @@ function createAdPodImp(bidRequest, videoMediaType) { function getAdPodNumberOfPlacements(videoMediaType) { const {adPodDurationSec, durationRangeSec, requireExactDuration} = videoMediaType - const minAllowedDuration = getMinValueFromArray(durationRangeSec) + const minAllowedDuration = Math.min(...durationRangeSec) const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration) return requireExactDuration diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index ca43c26ffd7..7edaaa36957 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -1,8 +1,14 @@ -import { deepAccess, deepClone, logError, isFn, isPlainObject } from '../src/utils.js'; +import { deepAccess, deepClone, isArrayOfNums, isFn, isInteger, isPlainObject, logError } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'smartadserver'; const GVL_ID = 45; const DEFAULT_FLOOR = 0.0; @@ -55,20 +61,53 @@ export const spec = { * Fills the payload with specific video attributes. * * @param {*} payload Payload that will be sent in the ServerRequest - * @param {*} videoMediaType Video media type. + * @param {*} videoMediaType Video media type */ fillPayloadForVideoBidRequest: function(payload, videoMediaType, videoParams) { const playerSize = videoMediaType.playerSize[0]; - payload.isVideo = videoMediaType.context === 'instream'; + const map = { + maxbitrate: 'vbrmax', + maxduration: 'vdmax', + minbitrate: 'vbrmin', + minduration: 'vdmin', + placement: 'vpt', + plcmt: 'vplcmt', + skip: 'skip' + }; + payload.mediaType = VIDEO; + payload.isVideo = videoMediaType.context === 'instream'; + payload.videoData = {}; + + for (const [key, value] of Object.entries(map)) { + payload.videoData = { + ...payload.videoData, + ...this.getValuableProperty(value, videoMediaType[key]) + }; + } + payload.videoData = { - videoProtocol: this.getProtocolForVideoBidRequest(videoMediaType, videoParams), - playerWidth: playerSize[0], - playerHeight: playerSize[1], - adBreak: this.getStartDelayForVideoBidRequest(videoMediaType, videoParams) + ...payload.videoData, + ...this.getValuableProperty('playerWidth', playerSize[0]), + ...this.getValuableProperty('playerHeight', playerSize[1]), + ...this.getValuableProperty('adBreak', this.getStartDelayForVideoBidRequest(videoMediaType, videoParams)), + ...this.getValuableProperty('videoProtocol', this.getProtocolForVideoBidRequest(videoMediaType, videoParams)), + ...(isArrayOfNums(videoMediaType.api) && videoMediaType.api.length ? { iabframeworks: videoMediaType.api.toString() } : {}), + ...(isArrayOfNums(videoMediaType.playbackmethod) && videoMediaType.playbackmethod.length ? { vpmt: videoMediaType.playbackmethod } : {}) }; }, + /** + * Gets a property object if the value not falsy + * @param {string} property + * @param {number} value + * @returns object with the property or empty + */ + getValuableProperty: function(property, value) { + return typeof property === 'string' && isInteger(value) && value + ? { [property]: value } : {}; + }, + /** * Gets the protocols from either videoParams or VideoMediaType * @param {*} videoMediaType @@ -93,18 +132,16 @@ export const spec = { * @returns positive integer value of startdelay */ getStartDelayForVideoBidRequest: function(videoMediaType, videoParams) { - if (videoParams !== undefined && videoParams.startDelay) { + if (videoParams?.startDelay) { return videoParams.startDelay; - } else if (videoMediaType !== undefined) { - if (videoMediaType.startdelay == 0) { - return 1; - } else if (videoMediaType.startdelay == -1) { + } else if (videoMediaType?.startdelay) { + if (videoMediaType.startdelay > 0 || videoMediaType.startdelay == -1) { return 2; } else if (videoMediaType.startdelay == -2) { return 3; } } - return 2;// Default value for all exotic cases set to bid.params.video.startDelay midroll hence 2. + return 1; // SADR-5619 }, /** diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index d91b62729bc..8394814365c 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -1,4 +1,17 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, generateUUID, isEmpty, _each, logMessage, logWarn, isFn, isPlainObject } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + generateUUID, + isEmpty, + _each, + logMessage, + logWarn, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; @@ -8,6 +21,12 @@ import { import { VIDEO } from '../src/mediaTypes.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'smartx'; const URL = 'https://bid.sxp.smartclip.net/bid/1000'; const GVLID = 115; diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md index 853f06d6baf..50f78660458 100644 --- a/modules/smartxBidAdapter.md +++ b/modules/smartxBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: smartclip Bidder Adapter Module Type: Bidder Adapter -Maintainer: adtech@smartclip.tv +Maintainer: bidding@smartclip.tv ``` # Description @@ -170,4 +170,4 @@ This adapter requires setup and approval from the smartclip team. } }], }]; -``` \ No newline at end of file +``` diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 7dbd2f3993a..6920983e50d 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -3,9 +3,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'smartyads'; -const AD_URL = 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'; +const GVLID = 534; +const adUrls = { + US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + SGP: 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' +} + const URL_SYNC = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a'; function isBidResponseValid(bid) { @@ -25,8 +32,36 @@ function isBidResponseValid(bid) { } } +function getAdUrlByRegion(bid) { + let adUrl; + + if (bid.params.region && adUrls[bid.params.region]) { + adUrl = adUrls[bid.params.region]; + } else { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + + switch (region) { + case 'Europe': + adUrl = adUrls['EU']; + break; + case 'Asia': + adUrl = adUrls['SGP']; + break; + default: adUrl = adUrls['US_EAST']; + } + } catch (err) { + adUrl = adUrls['US_EAST']; + } + } + + return adUrl; +} + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -47,6 +82,7 @@ export const spec = { location = winTop.location; logMessage(e); }; + let placements = []; let request = { 'deviceWidth': winTop.screen.width, @@ -56,7 +92,9 @@ export const spec = { 'host': location.host, 'page': location.pathname, 'coppa': config.getConfig('coppa') === true ? 1 : 0, - 'placements': placements + 'placements': placements, + 'eeid': validBidRequests[0]?.userIdAsEids, + 'ifa': bidderRequest?.ortb2?.device?.ifa, }; request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) if (bidderRequest) { @@ -72,9 +110,14 @@ export const spec = { } const len = validBidRequests.length; + let adUrl; + for (let i = 0; i < len; i++) { let bid = validBidRequests[i]; - let traff = bid.params.traffic || BANNER + + if (i === 0) adUrl = getAdUrlByRegion(bid); + + let traff = bid.params.traffic || BANNER; placements.push({ placementId: bid.params.sourceid, bidId: bid.bidId, @@ -86,11 +129,12 @@ export const spec = { placements.schain = bid.schain; } } + return { method: 'POST', - url: AD_URL, + url: adUrl, data: request - }; + } }, interpretResponse: (serverResponse) => { @@ -122,7 +166,29 @@ export const spec = { } return syncs - } + }, + + onBidWon: function(bid) { + if (bid.winUrl) { + ajax(bid.winUrl, () => {}, JSON.stringify(bid)); + } else { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); + } + } + }, + + onTimeout: function(bid) { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); + } + }, + + onBidderError: function(bid) { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); + } + }, }; diff --git a/modules/smartyadsBidAdapter.md b/modules/smartyadsBidAdapter.md index e0d6023a794..443d5ab5978 100644 --- a/modules/smartyadsBidAdapter.md +++ b/modules/smartyadsBidAdapter.md @@ -14,10 +14,11 @@ Module that connects to SmartyAds' demand sources | Name | Scope | Description | Example | | :------------ | :------- | :------------------------ | :------------------- | -| `sourceid` | required (for prebid.js) | placement ID | "0" | -| `host` | required (for prebid-server) | const value, set to "prebid" | "prebid" | -| `accountid` | required (for prebid-server) | partner ID | "1901" | +| `sourceid` | required (for prebid.js) | Placement ID | "0" | +| `host` | required (for prebid-server) | Const value, set to "prebid" | "prebid" | +| `accountid` | required (for prebid-server) | Partner ID | "1901" | | `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | +| `region` | optional (for prebid.js) | Prefix of the region to which prebid must send requests. Possible values: "US_EAST", "EU" | "US_EAST" | # Test Parameters ``` @@ -35,7 +36,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'native' + traffic: 'native', + region: 'US_EAST' + } } ] @@ -55,7 +58,8 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'banner' + traffic: 'banner', + region: 'US_EAST' } } ] @@ -76,7 +80,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'video' + traffic: 'video', + region: 'US_EAST' + } } ] diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index b7a25ba58df..7d4a4bca615 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -1,32 +1,69 @@ -import { isArray, logError, logWarn, isFn, isPlainObject } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {deepAccess, deepClone, isArray, isFn, isPlainObject, logError, logWarn} from '../src/utils.js'; +import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {convertOrtbRequestToProprietaryNative, toOrtbNativeRequest, toLegacyResponse} from '../src/native.js'; + +const BIDDER_CODE = 'smilewanted'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const GVL_ID = 639; export const spec = { - code: 'smilewanted', + code: BIDDER_CODE, + gvlid: GVL_ID, aliases: ['smile', 'sw'], - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. * - * @param {object} bid The bid to validate. + * @param {BidRequest} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { - return !!(bid.params && bid.params.zoneId); + if (!bid.params || !bid.params.zoneId) { + return false; + } + + if (deepAccess(bid, 'mediaTypes.video')) { + const videoMediaTypesParams = deepAccess(bid, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bid, 'params.video', {}); + + const videoParams = { + ...videoMediaTypesParams, + ...videoBidderParams + }; + + if (!videoParams.context || ![INSTREAM, OUTSTREAM].includes(videoParams.context)) { + return false; + } + } + + return true; }, /** * 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. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + return validBidRequests.map(bid => { - var payload = { + const payload = { zoneId: bid.params.zoneId, currencyCode: config.getConfig('currency.adServerCurrency') || 'EUR', tagId: bid.adUnitCode, @@ -37,11 +74,13 @@ export const spec = { transactionId: bid.ortb2Imp?.ext?.tid, timeout: bidderRequest?.timeout, bidId: bid.bidId, - /** positionType is undocumented + /** + positionType is undocumented It is unclear what this parameter means. If it means the same as pos in openRTB, It should read from openRTB object - or from mediaTypes.banner.pos */ + or from mediaTypes.banner.pos + */ positionType: bid.params.positionType || '', prebidVersion: '$prebid.version$' }; @@ -55,20 +94,41 @@ export const spec = { payload.bidfloor = bid.params.bidfloor; } - if (bidderRequest && bidderRequest.refererInfo) { + if (bidderRequest?.refererInfo) { payload.pageDomain = bidderRequest.refererInfo.page || ''; } - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest?.gdprConsent) { payload.gdpr_consent = bidderRequest.gdprConsent.consentString; payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } - if (bid && bid.userIdAsEids) { - payload.eids = bid.userIdAsEids; + payload.eids = bid?.userIdAsEids; + + const videoMediaType = deepAccess(bid, 'mediaTypes.video'); + const context = deepAccess(bid, 'mediaTypes.video.context'); + + if (bid.mediaType === 'video' || (videoMediaType && context === INSTREAM) || (videoMediaType && context === OUTSTREAM)) { + payload.context = context; + payload.videoParams = deepClone(videoMediaType); + } + + const nativeMediaType = deepAccess(bid, 'mediaTypes.native'); + + if (nativeMediaType) { + payload.context = 'native'; + payload.nativeParams = nativeMediaType; + let sizes = deepAccess(bid, 'mediaTypes.native.image.sizes', []); + + if (sizes.length > 0) { + const size = Array.isArray(sizes[0]) ? sizes[0] : sizes; + + payload.width = size[0] || payload.width; + payload.height = size[1] || payload.height; + } } - var payloadString = JSON.stringify(payload); + const payloadString = JSON.stringify(payload); return { method: 'POST', url: 'https://prebid.smilewanted.com', @@ -80,18 +140,21 @@ export const spec = { /** * Unpack the response from the server into a list of bids. * - * @param {*} serverResponse A successful response from the server. + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse.body) return []; const bidResponses = []; - var response = serverResponse.body; try { + const response = serverResponse.body; + const bidRequestData = JSON.parse(bidRequest.data); if (response) { const dealId = response.dealId || ''; const bidResponse = { - requestId: JSON.parse(bidRequest.data).bidId, + requestId: bidRequestData.bidId, cpm: response.cpm, width: response.width, height: response.height, @@ -103,14 +166,21 @@ export const spec = { ad: response.ad, }; - if (response.formatTypeSw == 'video_instream' || response.formatTypeSw == 'video_outstream') { + if (response.formatTypeSw === 'video_instream' || response.formatTypeSw === 'video_outstream') { bidResponse['mediaType'] = 'video'; bidResponse['vastUrl'] = response.ad; bidResponse['ad'] = null; + + if (response.formatTypeSw === 'video_outstream') { + bidResponse['renderer'] = newRenderer(bidRequestData, response); + } } - if (response.formatTypeSw == 'video_outstream') { - bidResponse['renderer'] = newRenderer(JSON.parse(bidRequest.data), response); + if (response.formatTypeSw === 'native') { + const nativeAdResponse = JSON.parse(response.ad); + const ortbNativeRequest = toOrtbNativeRequest(bidRequestData.nativeParams); + bidResponse['mediaType'] = 'native'; + bidResponse['native'] = toLegacyResponse(nativeAdResponse, ortbNativeRequest); } if (dealId.length > 0) { @@ -118,7 +188,7 @@ export const spec = { } bidResponse.meta = {}; - if (response.meta && response.meta.advertiserDomains && isArray(response.meta.advertiserDomains)) { + if (response.meta?.advertiserDomains && isArray(response.meta.advertiserDomains)) { bidResponse.meta.advertiserDomains = response.meta.advertiserDomains; } bidResponses.push(bidResponse); @@ -126,15 +196,18 @@ export const spec = { } catch (error) { logError('Error while parsing smilewanted response', error); } + return bidResponses; }, /** - * User syncs. + * Register the user sync pixels which should be dropped after the auction. * - * @param {*} syncOptions Publisher prebid configuration. - * @param {*} serverResponses A successful response from the server. - * @return {Syncs[]} An array of syncs that should be executed. + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} responses List of server's responses. + * @param {Object} gdprConsent The GDPR consent parameters + * @param {Object} uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { let params = ''; @@ -167,7 +240,8 @@ export const spec = { /** * Create SmileWanted renderer - * @param requestId + * @param bidRequest + * @param bidResponse * @returns {*} */ function newRenderer(bidRequest, bidResponse) { diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index f41fb98d436..5a327b05cd0 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -1,9 +1,10 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {deepAccess, isArray, isFn, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isPlainObject, inIframe, getDNT, generateUUID} from '../src/utils.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'snigel'; const GVLID = 1076; @@ -11,8 +12,14 @@ const DEFAULT_URL = 'https://adserv.snigelweb.com/bp/v1/prebid'; const DEFAULT_TTL = 60; const DEFAULT_CURRENCIES = ['USD']; const FLOOR_MATCH_ALL_SIZES = '*'; +const SESSION_ID_KEY = '_sn_session_pba'; const getConfig = config.getConfig; +const storageManager = getStorageManager({bidderCode: BIDDER_CODE}); +const refreshes = {}; +const pageViewId = generateUUID(); +const pageViewStart = new Date().getTime(); +let auctionCounter = 0; export const spec = { code: BIDDER_CODE, @@ -29,12 +36,20 @@ export const spec = { method: 'POST', url: getEndpoint(), data: JSON.stringify({ - id: bidderRequest.bidderRequestId, + id: bidderRequest.auctionId, + accountId: deepAccess(bidRequests, '0.params.accountId'), + site: deepAccess(bidRequests, '0.params.site'), + sessionId: getSessionId(), + counter: auctionCounter++, + pageViewId: pageViewId, + pageViewStart: pageViewStart, + gdprConsent: gdprApplies === true ? hasFullGdprConsent(deepAccess(bidderRequest, 'gdprConsent')) : false, cur: getCurrencies(), test: getTestFlag(), - devw: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, - devh: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, version: getGlobal().version, + gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || deepAccess(bidderRequest, 'ortb2.regs.gpp'), + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || deepAccess(bidderRequest, 'ortb2.regs.gpp_sid'), gdprApplies: gdprApplies, gdprConsentString: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.consentString') : undefined, gdprConsentProv: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.addtlConsent') : undefined, @@ -43,12 +58,24 @@ export const spec = { eids: deepAccess(bidRequests, '0.userIdAsEids'), schain: deepAccess(bidRequests, '0.schain'), page: getPage(bidderRequest), + topframe: inIframe() === true ? 0 : 1, + device: { + w: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, + h: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + dnt: getDNT() ? 1 : 0, + language: getLanguage(), + }, placements: bidRequests.map((r) => { return { - uuid: r.bidId, + id: r.adUnitCode, + tid: r.transactionId, + gpid: deepAccess(r, 'ortb2Imp.ext.gpid'), + pbadslot: deepAccess(r, 'ortb2Imp.ext.data.pbadslot') || deepAccess(r, 'ortb2Imp.ext.gpid'), name: r.params.placement, sizes: r.sizes, floor: getPriceFloor(r, BANNER, FLOOR_MATCH_ALL_SIZES), + refresh: getRefreshInformation(r.adUnitCode), + params: r.params.additionalParams, }; }), }), @@ -56,14 +83,14 @@ export const spec = { }; }, - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { if (!serverResponse.body || !serverResponse.body.bids) { return []; } return serverResponse.body.bids.map((bid) => { return { - requestId: bid.uuid, + requestId: mapIdToRequestId(bid.id, bidRequest), cpm: bid.price, creativeId: bid.crid, currency: serverResponse.body.cur, @@ -77,9 +104,9 @@ export const spec = { }); }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrl = getSyncUrl(responses || []); - if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent)) { + if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent, gppConsent)) { return [{type: 'iframe', url: getSyncEndpoint(syncUrl, gdprConsent)}]; } }, @@ -88,9 +115,7 @@ export const spec = { registerBidder(spec); function getPage(bidderRequest) { - return ( - getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || window.location.href - ); + return getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.page') || window.location.href; } function getEndpoint() { @@ -101,6 +126,14 @@ function getTestFlag() { return getConfig(`${BIDDER_CODE}.test`) === true; } +function getLanguage() { + return navigator && navigator.language + ? navigator.language.indexOf('-') != -1 + ? navigator.language.split('-')[0] + : navigator.language + : undefined; +} + function getCurrencies() { const currencyOverrides = getConfig(`${BIDDER_CODE}.cur`); if (currencyOverrides !== undefined && (!isArray(currencyOverrides) || currencyOverrides.length === 0)) { @@ -130,13 +163,55 @@ function getPriceFloor(bidRequest, mediaType, size) { } } -function hasSyncConsent(gdprConsent, uspConsent) { - if (gdprConsent?.gdprApplies && !hasPurpose1Consent(gdprConsent)) { - return false; - } else if (uspConsent && uspConsent[1] === 'Y' && uspConsent[2] === 'Y') { +function getRefreshInformation(adUnitCode) { + const refresh = refreshes[adUnitCode]; + if (!refresh) { + refreshes[adUnitCode] = { + count: 0, + previousTime: new Date(), + }; + return undefined; + } + + const currentTime = new Date(); + const timeDifferenceSeconds = Math.floor((currentTime - refresh.previousTime) / 1000); + refresh.count += 1; + refresh.previousTime = currentTime; + return { + count: refresh.count, + time: timeDifferenceSeconds, + }; +} + +function mapIdToRequestId(id, bidRequest) { + return bidRequest.bidderRequest.bids.filter((bid) => bid.adUnitCode === id)[0].bidId; +} + +function hasUspConsent(uspConsent) { + return typeof uspConsent !== 'string' || !(uspConsent[0] === '1' && uspConsent[2] === 'Y'); +} + +function hasGppConsent(gppConsent) { + return ( + !(gppConsent && Array.isArray(gppConsent.applicableSections)) || + gppConsent.applicableSections.every((section) => typeof section === 'number' && section <= 5) + ); +} + +function hasSyncConsent(gdprConsent, uspConsent, gppConsent) { + return hasPurpose1Consent(gdprConsent) && hasUspConsent(uspConsent) && hasGppConsent(gppConsent); +} + +function hasFullGdprConsent(gdprConsent) { + try { + const purposeConsents = Object.values(gdprConsent.vendorData.purpose.consents); + return ( + purposeConsents.length > 0 && + purposeConsents.every((value) => value === true) && + gdprConsent.vendorData.vendor.consents[GVLID] === true + ); + } catch (e) { return false; - } else { - return true; } } @@ -149,3 +224,20 @@ function getSyncEndpoint(url, gdprConsent) { gdprConsent?.consentString || '' )}`; } + +function getSessionId() { + try { + if (storageManager.localStorageIsEnabled()) { + let sessionId = storageManager.getDataFromLocalStorage(SESSION_ID_KEY); + if (sessionId == null) { + sessionId = generateUUID(); + storageManager.setDataInLocalStorage(SESSION_ID_KEY, sessionId); + } + return sessionId; + } else { + return undefined; + } + } catch (e) { + return undefined; + } +} diff --git a/modules/snigelBidAdapter.md b/modules/snigelBidAdapter.md index a83e133144f..f9bb1951d21 100644 --- a/modules/snigelBidAdapter.md +++ b/modules/snigelBidAdapter.md @@ -15,9 +15,13 @@ Please reach out to us [through our contact form](https://snigel.com/get-in-touc # Parameters -| Name | Required | Description | Example | -| :--- | :-------- | :---------- | :------ | -| placement | Yes | Placement identifier | top_leaderboard | +| Name | Required | Description | +| :-------- | :------- | :------------------- | +| accountId | Yes | Account identifier | +| site | Yes | Site identifier | +| placement | Yes | Placement identifier | + +Snigel will provide all of these parameters to you. # Test @@ -37,6 +41,8 @@ var adUnits = [ { bidder: "snigel", params: { + accountId: "1000", + site: "test.com", placement: "prebid_test_placement", }, }, diff --git a/modules/sonobiAnalyticsAdapter.js b/modules/sonobiAnalyticsAdapter.js index 0057944b201..04a855b5be6 100644 --- a/modules/sonobiAnalyticsAdapter.js +++ b/modules/sonobiAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {ajaxBuilder} from '../src/ajax.js'; let ajax = ajaxBuilder(0); -const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; +export const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; const analyticsType = 'endpoint'; const QUEUE_TIMEOUT_DEFAULT = 200; const { diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 704275cc1bf..e1b51affd09 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -1,11 +1,18 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; +import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { userSync } from '../src/userSync.js'; import { bidderSettings } from '../src/bidderSettings.js'; import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'sonobi'; const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; const PAGEVIEW_ID = generateUUID(); @@ -60,23 +67,15 @@ export const spec = { */ buildRequests: (validBidRequests, bidderRequest) => { const bids = validBidRequests.map(bid => { - let mediaType; - - if (deepAccess(bid, 'mediaTypes.video')) { - mediaType = 'video'; - } else if (deepAccess(bid, 'mediaTypes.banner')) { - mediaType = 'display'; - } - let slotIdentifier = _validateSlot(bid); if (/^[\/]?[\d]+[[\/].+[\/]?]?$/.test(slotIdentifier)) { slotIdentifier = slotIdentifier.charAt(0) === '/' ? slotIdentifier : '/' + slotIdentifier; return { - [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else if (/^[0-9a-fA-F]{20}$/.test(slotIdentifier) && slotIdentifier.length === 20) { return { - [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else { logError(`The ad unit code or Sonobi Placement id for slot ${bid.bidId} is invalid`); @@ -102,6 +101,8 @@ export const spec = { const fpd = bidderRequest.ortb2; if (fpd) { + delete fpd.experianRtidData; // Omit the experian data since we already pass this through a dedicated query param + delete fpd.experianRtidKey payload.fpd = JSON.stringify(fpd); } @@ -336,10 +337,28 @@ function _validateGPID(bid) { return '' } -function _validateMediaType(mediaType) { +function _validateMediaType(bidRequest) { + let mediaType; + if (deepAccess(bidRequest, 'mediaTypes.video')) { + mediaType = 'video'; + } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { + mediaType = 'display'; + } + let mediaTypeValidation = ''; if (mediaType === 'video') { mediaTypeValidation = 'c=v,'; + if (deepAccess(bidRequest, 'mediaTypes.video.playbackmethod')) { + mediaTypeValidation = `${mediaTypeValidation}pm=${deepAccess(bidRequest, 'mediaTypes.video.playbackmethod').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.placement')) { + let placement = deepAccess(bidRequest, 'mediaTypes.video.placement'); + mediaTypeValidation = `${mediaTypeValidation}p=${placement},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.plcmt')) { + let plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt'); + mediaTypeValidation = `${mediaTypeValidation}pl=${plcmt},`; + } } else if (mediaType === 'display') { mediaTypeValidation = 'c=d,'; } diff --git a/modules/sovrnAnalyticsAdapter.md b/modules/sovrnAnalyticsAdapter.md index 80bc6d7f6b1..b4fe7c971a2 100644 --- a/modules/sovrnAnalyticsAdapter.md +++ b/modules/sovrnAnalyticsAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Sovrn Analytics Adapter Module Type: Analytics Adapter -Maintainer: jrosendahl@sovrn.com +Maintainer: exchange@sovrn.com ``` # Description diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 0d077ad2ae3..64604618680 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,13 +1,12 @@ import { _each, - getBidIdParameter, isArray, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, - logWarn + logWarn, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { @@ -16,6 +15,10 @@ import { VIDEO } from '../src/mediaTypes.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), 'minduration': (value) => isInteger(value), @@ -45,7 +48,6 @@ const ORTB_VIDEO_PARAMS = { const REQUIRED_VIDEO_PARAMS = { context: (value) => value !== ADPOD, mimes: ORTB_VIDEO_PARAMS.mimes, - minduration: ORTB_VIDEO_PARAMS.minduration, maxduration: ORTB_VIDEO_PARAMS.maxduration, protocols: ORTB_VIDEO_PARAMS.protocols } @@ -105,18 +107,11 @@ export const spec = { } iv = iv || getBidIdParameter('iv', bid.params) - const floorInfo = (bid.getFloor && typeof bid.getFloor === 'function') ? bid.getFloor({ - currency: 'USD', - mediaType: bid.mediaTypes && bid.mediaTypes.banner ? 'banner' : 'video', - size: '*' - }) : {} - floorInfo.floor = floorInfo.floor || getBidIdParameter('bidfloor', bid.params) - const imp = { adunitcode: bid.adUnitCode, id: bid.bidId, tagid: String(getBidIdParameter('tagid', bid.params)), - bidfloor: floorInfo.floor + bidfloor: _getBidFloors(bid) } if (deepAccess(bid, 'mediaTypes.banner')) { @@ -174,6 +169,11 @@ export const spec = { deepSetValue(sovrnBidReq, 'source.tid', tid) } + const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa'); + if (coppa) { + deepSetValue(sovrnBidReq, 'regs.coppa', 1); + } + if (bidderRequest.gdprConsent) { deepSetValue(sovrnBidReq, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); deepSetValue(sovrnBidReq, 'user.ext.consent', bidderRequest.gdprConsent.consentString) @@ -211,7 +211,7 @@ export const spec = { * Format Sovrn responses as Prebid bid responses * @param {id, seatbid} sovrnResponse A successful response from Sovrn. * @return {Bid[]} An array of formatted bids. - */ + */ interpretResponse: function({ body: {id, seatbid} }) { if (!id || !seatbid || !Array.isArray(seatbid)) return [] @@ -325,4 +325,18 @@ function _buildVideoRequestObj(bid) { return videoObj } +function _getBidFloors(bid) { + const floorInfo = (bid.getFloor && typeof bid.getFloor === 'function') ? bid.getFloor({ + currency: 'USD', + mediaType: bid.mediaTypes && bid.mediaTypes.banner ? 'banner' : 'video', + size: '*' + }) : {} + const floorModuleValue = parseFloat(floorInfo.floor) + if (!isNaN(floorModuleValue)) { + return floorModuleValue + } + const paramValue = parseFloat(getBidIdParameter('bidfloor', bid.params)) + return !isNaN(paramValue) ? paramValue : undefined +} + registerBidder(spec) diff --git a/modules/sovrnBidAdapter.md b/modules/sovrnBidAdapter.md index 53e3158024d..ce131269eee 100644 --- a/modules/sovrnBidAdapter.md +++ b/modules/sovrnBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Sovrn Bid Adapter Module Type: Bidder Adapter -Maintainer: jrosendahl@sovrn.com +Maintainer: exchange@sovrn.com ``` # Description diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js new file mode 100644 index 00000000000..0bccc1ec140 --- /dev/null +++ b/modules/sparteoBidAdapter.js @@ -0,0 +1,170 @@ +import { deepAccess, deepSetValue, logError, parseSizesInput, triggerPixel } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + +const BIDDER_CODE = 'sparteo'; +const GVLID = 1028; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; +let isSynced = window.sparteoCrossfire?.started || false; + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: TTL // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + if (bidderRequest.bids[0].params.networkId) { + deepSetValue(request, 'site.publisher.ext.params.networkId', bidderRequest.bids[0].params.networkId); + } + + if (bidderRequest.bids[0].params.publisherId) { + deepSetValue(request, 'site.publisher.ext.params.publisherId', bidderRequest.bids[0].params.publisherId); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + deepSetValue(imp, 'ext.sparteo.params', bidRequest.params); + + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + context.mediaType = deepAccess(bid, 'ext.prebid.type'); + + const response = buildBidResponse(bid, context); + + if (context.mediaType == 'video') { + response.nurl = bid.nurl; + response.vastUrl = deepAccess(bid, 'ext.prebid.cache.vastXml.url') ?? null; + } + + return response; + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + let bannerParams = deepAccess(bid, 'mediaTypes.banner'); + let videoParams = deepAccess(bid, 'mediaTypes.video'); + + if (!bid.params) { + logError('The bid params are missing'); + return false; + } + + if (!bid.params.networkId && !bid.params.publisherId) { + logError('The networkId or publisherId is required'); + return false; + } + + if (!bannerParams && !videoParams) { + logError('The placement must be of banner or video type'); + return false; + } + + /** + * BANNER checks + */ + + if (bannerParams) { + let sizes = bannerParams.sizes; + + if (!sizes || parseSizesInput(sizes).length == 0) { + logError('mediaTypes.banner.sizes must be set for banner placement at the right format.'); + return false; + } + } + + /** + * VIDEO checks + */ + + if (videoParams) { + if (parseSizesInput(videoParams.playerSize).length == 0) { + logError('mediaTypes.video.playerSize must be set for video placement at the right format.'); + return false; + } + } + + return true; + }, + + buildRequests: function (bidRequests, bidderRequest) { + const payload = converter.toORTB({bidRequests, bidderRequest}) + + return { + method: HTTP_METHOD, + url: bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL, + data: payload + }; + }, + + interpretResponse: function (serverResponse, requests) { + const bids = converter.fromORTB({response: serverResponse.body, request: requests.data}).bids; + + return bids; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncurl = ''; + + if (!isSynced && !window.sparteoCrossfire?.started) { + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&usp_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + isSynced = true; + + window.sparteoCrossfire = { + started: true + }; + + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } + } + }, + + onTimeout: function (timeoutData) {}, + + onBidWon: function (bid) { + if (bid && bid.nurl) { + triggerPixel(bid.nurl, null); + } + }, + + onSetTargeting: function (bid) {} +}; + +registerBidder(spec); diff --git a/modules/sparteoBidAdapter.md b/modules/sparteoBidAdapter.md new file mode 100644 index 00000000000..774d9211d9d --- /dev/null +++ b/modules/sparteoBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Sparteo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@sparteo.com +``` + +# Description + +Module that connects to Sparteo's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + }, + bids: [ + { + bidder: 'sparteo', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 874207adcf8..c1f1c5159fc 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -1,10 +1,32 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, deepSetValue, isEmpty, _each, logMessage, logWarn, isBoolean, isNumber, isPlainObject, isFn, setScriptAttributes } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + deepSetValue, + isEmpty, + _each, + logMessage, + logWarn, + isBoolean, + isNumber, + isPlainObject, + isFn, + setScriptAttributes, + getBidIdParameter +} 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'; import { loadExternalScript } from '../src/adloader.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'spotx'; const URL = 'https://search.spotxchange.com/openrtb/2.3/dados/'; const ORTB_VERSION = '2.3'; diff --git a/modules/ssmasBidAdapter.js b/modules/ssmasBidAdapter.js new file mode 100644 index 00000000000..0b70a80e757 --- /dev/null +++ b/modules/ssmasBidAdapter.js @@ -0,0 +1,133 @@ +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel, deepSetValue } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import {config} from '../src/config.js'; + +export const SSMAS_CODE = 'ssmas'; +const SSMAS_SERVER = 'ads.ssmas.com'; +export const SSMAS_ENDPOINT = `https://${SSMAS_SERVER}/ortb`; +const SYNC_URL = `https://sync.ssmas.com/user_sync`; +export const SSMAS_REQUEST_METHOD = 'POST'; +const GDPR_VENDOR_ID = 1183; + +export const ssmasOrtbConverter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + mediaType: BANNER, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.placementId', bidRequest.params.placementId); + return imp; + }, +}); + +export const spec = { + code: SSMAS_CODE, + supportedMediaTypes: [BANNER], + gvlid: GDPR_VENDOR_ID, + + isBidRequestValid: (bid) => { + return !!bid.params.placementId && !!bid.bidId && bid.bidder === SSMAS_CODE; + }, + + buildRequests: (bidRequests, bidderRequest) => { + const data = ssmasOrtbConverter.toORTB({ bidRequests, bidderRequest }); + + const options = { + contentType: 'application/json', + withCredentials: false, + }; + + data.imp && data.imp.forEach(imp => { + if (imp.ext && imp.ext.placementId) { + imp.tagId = imp.ext.placementId; + } + }); + + data.regs = data.regs || {}; + data.regs.ext = data.regs.ext || {}; + + if (bidderRequest.gdprConsent) { + data.regs.ext.consent = bidderRequest.gdprConsent.consentString; + data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + if (bidderRequest.uspConsent) { + data.regs.ext.consent = bidderRequest.uspConsent.consentString; + data.regs.ext.ccpa = 1; + } + if (config.getConfig('coppa') === true) { + data.regs.coppa = 1; + } + + return [ + { + method: SSMAS_REQUEST_METHOD, + url: SSMAS_ENDPOINT, + data, + options, + }, + ]; + }, + + interpretResponse: (serverResponse, bidRequest) => { + const bids = ssmasOrtbConverter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + return bids.filter((bid) => { + return bid.cpm > 0; + }); + }, + + onBidWon: (bid) => { + if (bid.burl) { + triggerPixel(bid.burl); + } + }, + + getUserSyncs: ( + syncOptions, + serverResponses, + gdprConsent, + uspConsent + ) => { + const syncs = []; + + let params = ['pbjs=1']; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Boolean(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`); + } else { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + } + + if (uspConsent && uspConsent.consentString) { + params.push(`ccpa_consent=${uspConsent.consentString}`); + } + + if (syncOptions.iframeEnabled && serverResponses.length > 0) { + syncs.push({ + type: 'iframe', + url: `${SYNC_URL}/iframe?${params.join('&')}` + }); + } + + // if (syncOptions.pixelEnabled && serverResponses.length > 0) { + // syncs.push({ + // type: 'image', + // url: `${SYNC_URL}/image?${params.join('&')}` + // }); + // } + return syncs; + }, +}; + +registerBidder(spec); diff --git a/modules/ssmasBidAdapter.md b/modules/ssmasBidAdapter.md new file mode 100644 index 00000000000..1b15764a54d --- /dev/null +++ b/modules/ssmasBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +Module Name: SSMas Bidder Adapter +Module Type: Bidder Adapter +Maintainer: hzchen.work@gmail.com + +# Description + +Module that connects to Sem Seo & Mas header bidding endpoint to fetch bids. +Supports Banner +Supported currencies: EUR + +Required parameters: +- placement id + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'ssmas', + params: { + placementId: "10336" + } + }] + } +]; +``` diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 93ce26e7a48..08b25abee01 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -12,13 +12,14 @@ const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.8'; +const BIDDER_VERSION = '5.93'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; const oneCodeDetection = {}; const adUnitsCalled = {}; const adSizesCalled = {}; +const bidderRequestsMap = {}; const pageView = {}; var consentApiVersion; @@ -37,7 +38,7 @@ var nativeAssetMap = { /** * return native asset type, based on asset id - * @param {int} id - native asset id + * @param {number} id - native asset id * @returns {string} asset type */ const getNativeAssetType = id => { @@ -76,7 +77,7 @@ const getContentLanguage = () => { /** * Get Bid parameters - returns bid params from Object, or 1el array - * @param {*} bidData - bid (bidWon), or array of bids (timeout) + * @param {*} bidParams - bid (bidWon), or array of bids (timeout) * @returns {object} params object */ const unpackParams = (bidParams) => { @@ -93,19 +94,17 @@ const getNotificationPayload = bidData => { const bids = isArray(bidData) ? bidData : [bidData]; if (bids.length > 0) { let result = { - requestId: undefined, siteId: [], slotId: [], tagid: [], } bids.forEach(bid => { - const { adUnitCode, auctionId, cpm, creativeId, meta, params: bidParams, requestId, timeout } = bid; + const { adUnitCode, cpm, creativeId, meta, mediaType, params: bidParams, bidderRequestId, requestId, timeout } = bid; const params = unpackParams(bidParams); // basic notification data const bidBasicData = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: auctionId || result.requestId, + requestId: bidderRequestId || bidderRequestsMap[requestId], timeout: timeout || result.timeout, pvid: pageView.id, } @@ -127,12 +126,14 @@ const getNotificationPayload = bidData => { if (cpm) { // non-empty bid data + const { advertiserDomains = [], networkName, pricepl } = meta; const bidNonEmptyData = { cpm, - cpmpl: meta && meta.pricepl, + cpmpl: pricepl, creativeId, - adomain: meta && meta.advertiserDomains && meta.advertiserDomains[0], - networkName: meta && meta.networkName, + adomain: advertiserDomains[0], + adtype: mediaType, + networkName, } result = { ...result, ...bidNonEmptyData } } @@ -163,7 +164,7 @@ const applyClientHints = ortbRequest => { Check / generate page view id Should be generated dureing first call to applyClientHints(), and re-generated if pathname has changed - */ + */ if (!pageView.id || location.pathname !== pageView.path) { pageView.path = location.pathname; pageView.id = Math.floor(1E20 * Math.random()).toString(); @@ -198,9 +199,28 @@ const applyClientHints = ortbRequest => { ortbRequest.user = { ...ortbRequest.user, ...ch }; }; +const applyTopics = (validBidRequest, ortbRequest) => { + const userData = validBidRequest.ortb2?.user?.data || []; + const topicsData = userData.filter(dataObj => { + const segtax = dataObj.ext?.segtax; + return segtax >= 600 && segtax <= 609; + })[0]; + + // format topics obj for exchange + if (topicsData) { + topicsData.id = `${topicsData.ext.segtax}`; + topicsData.name = 'topics'; + delete (topicsData.ext); + ortbRequest.user.data.push(topicsData); + } +}; + const applyUserIds = (validBidRequest, ortbRequest) => { - const eids = validBidRequest.userIdAsEids - if (eids && eids.length) { + const { userIdAsEids: eidsVbr = [], ortb2 = {} } = validBidRequest; + const eidsOrtb = ortb2.user?.ext?.data?.eids || []; + const eids = [...eidsVbr, ...eidsOrtb]; + + if (eids.length) { const ids = { eids }; ortbRequest.user = { ...ortbRequest.user, ...ids }; } @@ -226,7 +246,7 @@ const applyGdpr = (bidderRequest, ortbRequest) => { * returns floor = 0 if getFloor() is not defined * * @param {object} slot bid request adslot - * @returns {float} floorprice + * @returns {number} floorprice */ const getHighestFloor = (slot) => { const currency = getCurrency(); @@ -447,10 +467,15 @@ var mapVideo = (slot, videoFromBid) => { }; const mapImpression = slot => { - const { adUnitCode, bidId, params = {}, ortb2Imp = {} } = slot; + const { adUnitCode, bidderRequestId, bidId, params = {}, ortb2Imp = {} } = slot; const { id, siteId, video } = params; const { ext = {} } = ortb2Imp; + /* + store bidId <-> bidderRequestId mapping for bidWon notification + */ + bidderRequestsMap[bidId] = bidderRequestId; + /* check max size for this imp, and check/store number this size was called (for current view) send this info as ext.pbsize @@ -484,7 +509,7 @@ const mapImpression = slot => { } const isVideoAd = bid => { - const xmlTester = new RegExp(/^<\?xml/); + const xmlTester = new RegExp(/^<\?xml| { return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } -const parseNative = (nativeData) => { +const parseNative = (nativeData, adUnitCode) => { const { link = {}, imptrackers: impressionTrackers, jstracker } = nativeData; const { url: clickUrl, clicktrackers: clickTrackers = [] } = link; + const macroReplacer = tracker => tracker.replace(new RegExp('%native_dom_id%', 'g'), adUnitCode); + let javascriptTrackers = isArray(jstracker) ? jstracker : jstracker && [jstracker]; + + // replace known macros in js trackers + javascriptTrackers = javascriptTrackers && javascriptTrackers.map(macroReplacer); const result = { clickUrl, clickTrackers, impressionTrackers, - javascriptTrackers: isArray(jstracker) ? jstracker : jstracker && [jstracker], + javascriptTrackers, }; nativeData.assets.forEach(asset => { @@ -543,6 +573,7 @@ const parseNative = (nativeData) => { } const renderCreative = (site, auctionId, bid, seat, request) => { + const { adLabel, id, slot, sn, page, publisherId, ref } = site; let gam; const mcad = { @@ -592,16 +623,16 @@ const renderCreative = (site, auctionId, bid, seat, request) => { } `; + iframe.onload = () => resolve(iframe.contentWindow.render); + document.body.appendChild(iframe); + }) + } + return renderers[src]; + } +})(); diff --git a/src/events.js b/src/events.js index 62f8c070deb..d98991180bf 100644 --- a/src/events.js +++ b/src/events.js @@ -3,23 +3,38 @@ */ import * as utils from './utils.js' import CONSTANTS from './constants.json'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {config} from './config.js'; +const TTL_CONFIG = 'eventHistoryTTL'; -var slice = Array.prototype.slice; -var push = Array.prototype.push; +let eventTTL = null; -// define entire events -// var allEvents = ['bidRequested','bidResponse','bidWon','bidTimeout']; -var allEvents = utils._map(CONSTANTS.EVENTS, function (v) { - return v; +// keep a record of all events fired +const eventsFired = ttlCollection({ + monotonic: true, + ttl: () => eventTTL, +}) + +config.getConfig(TTL_CONFIG, (val) => { + const previous = eventTTL; + val = val?.[TTL_CONFIG]; + eventTTL = typeof val === 'number' ? val * 1000 : null; + if (previous !== eventTTL) { + eventsFired.refresh(); + } }); -var idPaths = CONSTANTS.EVENT_ID_PATHS; +let slice = Array.prototype.slice; +let push = Array.prototype.push; + +// define entire events +let allEvents = Object.values(CONSTANTS.EVENTS); + +const idPaths = CONSTANTS.EVENT_ID_PATHS; -// keep a record of all events fired -var eventsFired = []; const _public = (function () { - var _handlers = {}; - var _public = {}; + let _handlers = {}; + let _public = {}; /** * @@ -30,31 +45,30 @@ const _public = (function () { function _dispatch(eventString, args) { utils.logMessage('Emitting event for: ' + eventString); - var eventPayload = args[0] || {}; - var idPath = idPaths[eventString]; - var key = eventPayload[idPath]; - var event = _handlers[eventString] || { que: [] }; - var eventKeys = utils._map(event, function (v, k) { - return k; - }); + let eventPayload = args[0] || {}; + let idPath = idPaths[eventString]; + let key = eventPayload[idPath]; + let event = _handlers[eventString] || { que: [] }; + var eventKeys = Object.keys(event); - var callbacks = []; + let callbacks = []; // record the event: - eventsFired.push({ + eventsFired.add({ eventType: eventString, args: eventPayload, id: key, elapsedTime: utils.getPerformanceNow(), }); - /** Push each specific callback to the `callbacks` array. + /** + * Push each specific callback to the `callbacks` array. * If the `event` map has a key that matches the value of the * event payload id path, e.g. `eventPayload[idPath]`, then apply * each function in the `que` array as an argument to push to the * `callbacks` array - * */ - if (key && utils.contains(eventKeys, key)) { + */ + if (key && eventKeys.includes(key)) { push.apply(callbacks, event[key].que); } @@ -62,24 +76,26 @@ const _public = (function () { push.apply(callbacks, event.que); /** call each of the callbacks */ - utils._each(callbacks, function (fn) { + (callbacks || []).forEach(function (fn) { if (!fn) return; try { fn.apply(null, args); } catch (e) { - utils.logError('Error executing handler:', 'events.js', e); + utils.logError('Error executing handler:', 'events.js', e, eventString); } }); } function _checkAvailableEvent(event) { - return utils.contains(allEvents, event); + return allEvents.includes(event) } + _public.has = _checkAvailableEvent; + _public.on = function (eventString, handler, id) { // check whether available event or not if (_checkAvailableEvent(eventString)) { - var event = _handlers[eventString] || { que: [] }; + let event = _handlers[eventString] || { que: [] }; if (id) { event[id] = event[id] || { que: [] }; @@ -95,12 +111,12 @@ const _public = (function () { }; _public.emit = function (event) { - var args = slice.call(arguments, 1); + let args = slice.call(arguments, 1); _dispatch(event, args); }; _public.off = function (eventString, handler, id) { - var event = _handlers[eventString]; + let event = _handlers[eventString]; if (utils.isEmpty(event) || (utils.isEmpty(event.que) && utils.isEmpty(event[id]))) { return; @@ -111,15 +127,15 @@ const _public = (function () { } if (id) { - utils._each(event[id].que, function (_handler) { - var que = event[id].que; + (event[id].que || []).forEach(function (_handler) { + let que = event[id].que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } }); } else { - utils._each(event.que, function (_handler) { - var que = event.que; + (event.que || []).forEach(function (_handler) { + let que = event.que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } @@ -142,13 +158,7 @@ const _public = (function () { * @return {Array} array of events fired */ _public.getEvents = function () { - var arrayCopy = []; - utils._each(eventsFired, function (value) { - var newProp = Object.assign({}, value); - arrayCopy.push(newProp); - }); - - return arrayCopy; + return eventsFired.toArray().map(val => Object.assign({}, val)) }; return _public; @@ -156,8 +166,8 @@ const _public = (function () { utils._setEventEmitter(_public.emit.bind(_public)); -export const {on, off, get, getEvents, emit, addEvents} = _public; +export const {on, off, get, getEvents, emit, addEvents, has} = _public; export function clearEvents() { - eventsFired.length = 0; + eventsFired.clear(); } diff --git a/src/fpd/enrichment.js b/src/fpd/enrichment.js index f812d8435d9..49e2f7d7cad 100644 --- a/src/fpd/enrichment.js +++ b/src/fpd/enrichment.js @@ -6,6 +6,10 @@ import {config} from '../config.js'; import {getHighEntropySUA, getLowEntropySUA} from './sua.js'; import {GreedyPromise} from '../utils/promise.js'; import {CLIENT_SECTIONS, clientSectionChecker, hasSection} from './oneClient.js'; +import {isActivityAllowed} from '../activities/rules.js'; +import {activityParams} from '../activities/activityParams.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../activities/activities.js'; +import {MODULE_TYPE_PREBID} from '../activities/modules.js'; export const dep = { getRefererInfo, @@ -24,8 +28,10 @@ const oneClient = clientSectionChecker('FPD') * @returns: {Promise[{}]}: a promise to an enriched ortb2 object. */ export const enrichFPD = hook('sync', (fpd) => { - return GreedyPromise.all([fpd, getSUA().catch(() => null)]) - .then(([ortb2, sua]) => { + const promArr = [fpd, getSUA().catch(() => null), tryToGetCdepLabel().catch(() => null)]; + + return GreedyPromise.all(promArr) + .then(([ortb2, sua, cdep]) => { const ri = dep.getRefererInfo(); mergeLegacySetConfigs(ortb2); Object.entries(ENRICHMENTS).forEach(([section, getEnrichments]) => { @@ -34,9 +40,18 @@ export const enrichFPD = hook('sync', (fpd) => { ortb2[section] = mergeDeep({}, data, ortb2[section]); } }); + if (sua) { deepSetValue(ortb2, 'device.sua', Object.assign({}, sua, ortb2.device.sua)); } + + if (cdep) { + const ext = { + cdep + } + deepSetValue(ortb2, 'device.ext', Object.assign({}, ext, ortb2.device.ext)); + } + ortb2 = oneClient(ortb2); for (let section of CLIENT_SECTIONS) { if (hasSection(ortb2, section)) { @@ -44,6 +59,7 @@ export const enrichFPD = hook('sync', (fpd) => { break; } } + return ortb2; }); }); @@ -78,6 +94,10 @@ function removeUndef(obj) { return getDefinedParams(obj, Object.keys(obj)) } +function tryToGetCdepLabel() { + return GreedyPromise.resolve('cookieDeprecationLabel' in navigator && isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && navigator.cookieDeprecationLabel.getValue()); +} + const ENRICHMENTS = { site(ortb2, ri) { if (CLIENT_SECTIONS.filter(p => p !== 'site').some(hasSection.bind(null, ortb2))) { @@ -93,13 +113,20 @@ const ENRICHMENTS = { return winFallback((win) => { const w = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; const h = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; - return { + + const device = { w, h, dnt: getDNT() ? 1 : 0, ua: win.navigator.userAgent, language: win.navigator.language.split('-').shift(), }; + + if (win.navigator?.webdriver) { + deepSetValue(device, 'ext.webdriver', true); + } + + return device; }) }, regs() { diff --git a/src/mediaTypes.js b/src/mediaTypes.js index eea286f7af5..2afa2aefaf9 100644 --- a/src/mediaTypes.js +++ b/src/mediaTypes.js @@ -10,11 +10,11 @@ * @typedef {('adpod')} VideoContext */ -/** @type MediaType */ +/** @type {MediaType} */ export const NATIVE = 'native'; -/** @type MediaType */ +/** @type {MediaType} */ export const VIDEO = 'video'; -/** @type MediaType */ +/** @type {MediaType} */ export const BANNER = 'banner'; -/** @type VideoContext */ +/** @type {VideoContext} */ export const ADPOD = 'adpod'; diff --git a/src/native.js b/src/native.js index 927423c8d72..1b6e13c77fc 100644 --- a/src/native.js +++ b/src/native.js @@ -1,7 +1,6 @@ import { deepAccess, - deepClone, - getKeyByValue, + deepClone, getDefinedParams, insertHtmlIntoIframe, isArray, isBoolean, @@ -17,6 +16,11 @@ import {auctionManager} from './auctionManager.js'; import CONSTANTS from './constants.json'; import {NATIVE} from './mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + export const nativeAdapters = []; export const NATIVE_TARGETING_KEYS = Object.keys(CONSTANTS.NATIVE_KEYS).map( @@ -100,6 +104,12 @@ const TRACKER_EVENTS = { 'viewable-video50': 4, } +export function isNativeResponse(bidResponse) { + // check for native data and not mediaType; it's possible + // to treat banner responses as native + return bidResponse.native && typeof bidResponse.native === 'object'; +} + /** * Recieves nativeParams from an adUnit. If the params were not of type 'type', * passes them on directly. If they were of type 'type', translate @@ -328,6 +338,23 @@ export function fireClickTrackers(nativeResponse, assetId = null, {fetchURL = tr } } +export function setNativeResponseProperties(bid, adUnit) { + const nativeOrtbRequest = adUnit?.nativeOrtbRequest; + const nativeOrtbResponse = bid.native?.ortb; + + if (nativeOrtbRequest && nativeOrtbResponse) { + const legacyResponse = toLegacyResponse(nativeOrtbResponse, nativeOrtbRequest); + Object.assign(bid.native, legacyResponse); + } + + ['rendererUrl', 'adTemplate'].forEach(prop => { + const val = adUnit?.nativeParams?.[prop]; + if (val) { + bid.native[prop] = getAssetValue(val); + } + }); +} + /** * Gets native targeting key-value pairs * @param {Object} bid @@ -336,11 +363,6 @@ export function fireClickTrackers(nativeResponse, assetId = null, {fetchURL = tr export function getNativeTargeting(bid, {index = auctionManager.index} = {}) { let keyValues = {}; const adUnit = index.getAdUnit(bid); - if (deepAccess(adUnit, 'nativeParams.rendererUrl')) { - bid['native']['rendererUrl'] = getAssetValue(adUnit.nativeParams['rendererUrl']); - } else if (deepAccess(adUnit, 'nativeParams.adTemplate')) { - bid['native']['adTemplate'] = getAssetValue(adUnit.nativeParams['adTemplate']); - } const globalSendTargetingKeys = deepAccess( adUnit, @@ -385,49 +407,50 @@ export function getNativeTargeting(bid, {index = auctionManager.index} = {}) { return keyValues; } -function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {}) { - const message = { - message: 'assetResponse', - adId: data.adId, - }; - - const adUnit = index.getAdUnit(adObject); - let nativeResp = adObject.native; +function getNativeAssets(nativeProps, keys, ext = false) { + let assets = []; + Object.entries(nativeProps) + .filter(([k, v]) => v && ((ext === false && k === 'ext') || keys == null || keys.includes(k))) + .forEach(([key, value]) => { + if (ext === false && key === 'ext') { + assets.push(...getNativeAssets(value, keys, true)); + } else if (ext || NATIVE_KEYS.hasOwnProperty(key)) { + assets.push({key, value: getAssetValue(value)}); + } + }); + return assets; +} - if (adObject.native.ortb) { - message.ortb = adObject.native.ortb; +export function getNativeRenderingData(bid, adUnit, keys) { + const data = { + ...getDefinedParams(bid.native, ['rendererUrl', 'adTemplate']), + assets: getNativeAssets(bid.native, keys), + nativeKeys: CONSTANTS.NATIVE_KEYS + }; + if (bid.native.ortb) { + data.ortb = bid.native.ortb; } else if (adUnit.mediaTypes?.native?.ortb) { - message.ortb = toOrtbNativeResponse(adObject.native, adUnit.nativeOrtbRequest); + data.ortb = toOrtbNativeResponse(bid.native, adUnit.nativeOrtbRequest); } - message.assets = []; - - (keys == null ? Object.keys(nativeResp) : keys).forEach(function(key) { - if (key === 'adTemplate' && nativeResp[key]) { - message.adTemplate = getAssetValue(nativeResp[key]); - } else if (key === 'rendererUrl' && nativeResp[key]) { - message.rendererUrl = getAssetValue(nativeResp[key]); - } else if (key === 'ext') { - Object.keys(nativeResp[key]).forEach(extKey => { - if (nativeResp[key][extKey]) { - const value = getAssetValue(nativeResp[key][extKey]); - message.assets.push({ key: extKey, value }); - } - }) - } else if (nativeResp[key] && CONSTANTS.NATIVE_KEYS.hasOwnProperty(key)) { - const value = getAssetValue(nativeResp[key]); + return data; +} - message.assets.push({ key, value }); - } - }); - return message; +function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {}) { + return { + message: 'assetResponse', + adId: data.adId, + ...getNativeRenderingData(adObject, index.getAdUnit(adObject), keys) + }; } +const NATIVE_KEYS_INVERTED = Object.fromEntries(Object.entries(CONSTANTS.NATIVE_KEYS).map(([k, v]) => [v, k])); + /** * Constructs a message object containing asset values for each of the * requested data keys. */ export function getAssetMessage(data, adObject) { - const keys = data.assets.map((k) => getKeyByValue(CONSTANTS.NATIVE_KEYS, k)); + const keys = data.assets.map((k) => NATIVE_KEYS_INVERTED[k]); return assetsMessage(data, adObject, keys); } @@ -480,6 +503,11 @@ export function toOrtbNativeRequest(legacyNativeAssets) { continue; } + if (key === 'privacyLink') { + ortb.privacy = 1; + continue; + } + const asset = legacyNativeAssets[key]; let required = 0; if (asset.required && isBoolean(asset.required)) { @@ -623,6 +651,9 @@ export function fromOrtbNativeRequest(openRTBRequest) { oldNativeObject[prebidAssetName].len = asset.data.len; } } + if (openRTBRequest.privacy) { + oldNativeObject.privacyLink = { required: false }; + } // video was not supported by old prebid assets } return oldNativeObject; @@ -696,8 +727,11 @@ export function legacyPropertiesToOrtbNative(legacyNative) { // in general, native trackers seem to be neglected and/or broken response.jstracker = Array.isArray(value) ? value.join('') : value; break; + case 'privacyLink': + response.privacy = value; + break; } - }) + }); return response; } @@ -780,8 +814,8 @@ export function toLegacyResponse(ortbResponse, ortbRequest) { legacyResponse.impressionTrackers = []; let jsTrackers = []; - if (ortbRequest?.imptrackers) { - legacyResponse.impressionTrackers.push(...ortbRequest.imptrackers); + if (ortbResponse.imptrackers) { + legacyResponse.impressionTrackers.push(...ortbResponse.imptrackers); } for (const eventTracker of ortbResponse?.eventtrackers || []) { if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) { diff --git a/src/prebid.js b/src/prebid.js index b949ece65ea..750a4bdee1a 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -2,19 +2,11 @@ import {getGlobal} from './prebidGlobal.js'; import { - adUnitsFilter, - bind, - callBurl, - contains, - createInvisibleIframe, deepAccess, deepClone, deepSetValue, flatten, generateUUID, - getHighestCpm, - inIframe, - insertElement, isArray, isArrayOfNums, isEmpty, @@ -26,8 +18,6 @@ import { logMessage, logWarn, mergeDeep, - replaceAuctionPrice, - replaceClickThrough, transformAdServerTargetingObj, uniques, unsupportedBidderMessage @@ -41,23 +31,24 @@ import {hook, wrapHook} from './hook.js'; import {loadSession} from './debugging.js'; import {includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; -import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; -import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; -import {default as adapterManager, gdprDataHandler, getS2SBidderSet, gppDataHandler, uspDataHandler} from './adapterManager.js'; +import {default as adapterManager, getS2SBidderSet} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; import {defer, GreedyPromise} from './utils/promise.js'; import {enrichFPD} from './fpd/enrichment.js'; +import {allConsent} from './consentHandler.js'; +import {renderAdDirect} from './adRendering.js'; +import {getHighestCpm} from './utils/reducers.js'; +import {fillVideoDefaults} from './video.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; /* private variables */ -const { ADD_AD_UNITS, BID_WON, REQUEST_BIDS, SET_TARGETING, STALE_RENDER } = CONSTANTS.EVENTS; -const { PREVENT_WRITING_ON_MAIN_DOCUMENT, NO_AD, EXCEPTION, CANNOT_FIND_AD, MISSING_DOC_OR_ADID } = CONSTANTS.AD_RENDER_FAILED_REASON; +const { ADD_AD_UNITS, REQUEST_BIDS, SET_TARGETING } = CONSTANTS.EVENTS; const eventValidators = { bidWon: checkDefinedPlacement @@ -89,7 +80,7 @@ function checkDefinedPlacement(id) { .reduce(flatten) .filter(uniques); - if (!contains(adUnitCodes, id)) { + if (!adUnitCodes.includes(id)) { logError('The "' + id + '" placement is not defined.'); return; } @@ -97,13 +88,6 @@ function checkDefinedPlacement(id) { return true; } -function setRenderSize(doc, width, height) { - if (doc.defaultView && doc.defaultView.frameElement) { - doc.defaultView.frameElement.width = width; - doc.defaultView.frameElement.height = height; - } -} - function validateSizes(sizes, targLength) { let cleanSizes = []; if (isArray(sizes) && ((targLength) ? sizes.length === targLength : sizes.length > 0)) { @@ -268,6 +252,12 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { return validatedAdUnits; }, 'checkAdUnitSetup'); +function fillAdUnitDefaults(adUnits) { + if (FEATURES.VIDEO) { + adUnits.forEach(au => fillVideoDefaults(au)) + } +} + /// /////////////////////////////// // // // Start Public APIs // @@ -330,28 +320,14 @@ pbjsInstance.getAdserverTargeting = function (adUnitCode) { return targeting.getAllTargeting(adUnitCode); }; -/** - * returns all consent data - * @return {Object} Map of consent types and data - * @alias module:pbjs.getConsentData - */ -function getConsentMetadata() { - return { - gdpr: gdprDataHandler.getConsentMeta(), - usp: uspDataHandler.getConsentMeta(), - gpp: gppDataHandler.getConsentMeta(), - coppa: !!(config.getConfig('coppa')) - } -} - pbjsInstance.getConsentMetadata = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getConsentMetadata'); - return getConsentMetadata(); + return allConsent.getConsentMeta() }; function getBids(type) { const responses = auctionManager[type]() - .filter(bind.call(adUnitsFilter, this, auctionManager.getAdUnitCodes())); + .filter(bid => auctionManager.getAdUnitCodes().includes(bid.adUnitCode)) // find the last auction id to get responses for most recent auction only const currentAuctionId = auctionManager.getLastAuctionId(); @@ -467,19 +443,6 @@ pbjsInstance.setTargetingForAst = function (adUnitCodes) { events.emit(SET_TARGETING, targeting.getAllTargeting()); }; -/** - * This function will check for presence of given node in given parent. If not present - will inject it. - * @param {Node} node node, whose existance is in question - * @param {Document} doc document element do look in - * @param {string} tagName tag name to look in - */ -function reinjectNodeIfRemoved(node, doc, tagName) { - const injectionNode = doc.querySelector(tagName); - if (!node.parentNode || node.parentNode !== injectionNode) { - insertElement(node, doc, tagName); - } -} - /** * This function will render the ad (based on params) in the given iframe document passed through. * Note that doc SHOULD NOT be the parent document page as we can't doc.write() asynchronously @@ -490,103 +453,7 @@ function reinjectNodeIfRemoved(node, doc, tagName) { pbjsInstance.renderAd = hook('async', function (doc, id, options) { logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); logMessage('Calling renderAd with adId :' + id); - - if (!id) { - const message = `Error trying to write ad Id :${id} to the page. Missing adId`; - emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id }); - return; - } - - try { - // lookup ad by ad Id - const bid = auctionManager.findBidByAdId(id); - if (!bid) { - const message = `Error trying to write ad. Cannot find ad by given id : ${id}`; - emitAdRenderFail({ reason: CANNOT_FIND_AD, message, id }); - return; - } - - if (bid.status === CONSTANTS.BID_STATUS.RENDERED) { - logWarn(`Ad id ${bid.adId} has been rendered before`); - events.emit(STALE_RENDER, bid); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } - } - - // replace macros according to openRTB with price paid = bid.cpm - bid.ad = replaceAuctionPrice(bid.ad, bid.originalCpm || bid.cpm); - bid.adUrl = replaceAuctionPrice(bid.adUrl, bid.originalCpm || bid.cpm); - // replacing clickthrough if submitted - if (options && options.clickThrough) { - const {clickThrough} = options; - bid.ad = replaceClickThrough(bid.ad, clickThrough); - bid.adUrl = replaceClickThrough(bid.adUrl, clickThrough); - } - - // save winning bids - auctionManager.addWinningBid(bid); - - // emit 'bid won' event here - events.emit(BID_WON, bid); - - const {height, width, ad, mediaType, adUrl, renderer} = bid; - - // video module - if (FEATURES.VIDEO) { - const adUnitCode = bid.adUnitCode; - const adUnit = pbjsInstance.adUnits.filter(adUnit => adUnit.code === adUnitCode); - const videoModule = pbjsInstance.videoModule; - if (adUnit.video && videoModule) { - videoModule.renderBid(adUnit.video.divId, bid); - return; - } - } - - if (!doc) { - const message = `Error trying to write ad Id :${id} to the page. Missing document`; - emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id }); - return; - } - - const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`); - insertElement(creativeComment, doc, 'html'); - - if (isRendererRequired(renderer)) { - executeRenderer(renderer, bid, doc); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - emitAdRenderSucceeded({ doc, bid, id }); - } else if ((doc === document && !inIframe()) || mediaType === 'video') { - const message = `Error trying to write ad. Ad render call ad id ${id} was prevented from writing to the main document.`; - emitAdRenderFail({reason: PREVENT_WRITING_ON_MAIN_DOCUMENT, message, bid, id}); - } else if (ad) { - doc.write(ad); - doc.close(); - setRenderSize(doc, width, height); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - callBurl(bid); - emitAdRenderSucceeded({ doc, bid, id }); - } else if (adUrl) { - const iframe = createInvisibleIframe(); - iframe.height = height; - iframe.width = width; - iframe.style.display = 'inline'; - iframe.style.overflow = 'hidden'; - iframe.src = adUrl; - - insertElement(iframe, doc, 'body'); - setRenderSize(doc, width, height); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - callBurl(bid); - emitAdRenderSucceeded({ doc, bid, id }); - } else { - const message = `Error trying to write ad. No ad for bid response id: ${id}`; - emitAdRenderFail({reason: NO_AD, message, bid, id}); - } - } catch (e) { - const message = `Error trying to write ad Id :${id} to the page:${e.message}`; - emitAdRenderFail({ reason: EXCEPTION, message, id }); - } + renderAdDirect(doc, id, options); }); /** @@ -671,6 +538,7 @@ pbjsInstance.requestBids = (function() { export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, ttlBuffer, adUnitCodes, labels, auctionId, ortb2Fragments, metrics, defer } = {}) { const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); + fillAdUnitDefaults(adUnits); adUnits = useMetrics(metrics).measureTime('requestBids.validate', () => checkAdUnitSetup(adUnits)); function auctionDone(bids, timedOut, auctionId) { @@ -684,6 +552,8 @@ export const startAuction = hook('async', function ({ bidsBackHandler, timeout: defer.resolve({bids, timedOut, auctionId}) } + const tids = {}; + /* * for a given adunit which supports a set of mediaTypes * and a given bidder which supports a set of mediaTypes @@ -699,15 +569,18 @@ export const startAuction = hook('async', function ({ bidsBackHandler, timeout: const bidderRegistry = adapterManager.bidderRegistry; const bidders = allBidders.filter(bidder => !s2sBidders.has(bidder)); - - const tid = adUnit.ortb2Imp?.ext?.tid || generateUUID(); - adUnit.transactionId = tid; + adUnit.adUnitId = generateUUID(); + const tid = adUnit.ortb2Imp?.ext?.tid; + if (tid) { + if (tids.hasOwnProperty(adUnit.code)) { + logWarn(`Multiple distinct ortb2Imp.ext.tid were provided for twin ad units '${adUnit.code}'`) + } else { + tids[adUnit.code] = tid; + } + } if (ttlBuffer != null && !adUnit.hasOwnProperty('ttlBuffer')) { adUnit.ttlBuffer = ttlBuffer; } - // Populate ortb2Imp.ext.tid with transactionId. Specifying a transaction ID per item in the ortb impression array, lets multiple transaction IDs be transmitted in a single bid request. - deepSetValue(adUnit, 'ortb2Imp.ext.tid', tid); - bidders.forEach(bidder => { const adapter = bidderRegistry[bidder]; const spec = adapter && adapter.getSpec && adapter.getSpec(); @@ -726,11 +599,18 @@ export const startAuction = hook('async', function ({ bidsBackHandler, timeout: }); adunitCounter.incrementRequestsCounter(adUnit.code); }); - if (!adUnits || adUnits.length === 0) { logMessage('No adUnits configured. No bids requested.'); auctionDone(); } else { + adUnits.forEach(au => { + const tid = au.ortb2Imp?.ext?.tid || tids[au.code] || generateUUID(); + if (!tids.hasOwnProperty(au.code)) { + tids[au.code] = tid; + } + au.transactionId = tid; + deepSetValue(au, 'ortb2Imp.ext.tid', tid); + }); const auction = auctionManager.createAuction({ adUnits, adUnitCodes, diff --git a/src/secureCreatives.js b/src/secureCreatives.js index c719bc191f2..1880f56f474 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,28 +4,27 @@ */ import * as events from './events.js'; -import {fireNativeTrackers, getAllAssetsMessage, getAssetMessage} from './native.js'; -import constants from './constants.json'; -import {deepAccess, isApnGetTagDefined, isGptPubadsDefined, logError, logWarn, replaceAuctionPrice} from './utils.js'; +import {getAllAssetsMessage, getAssetMessage} from './native.js'; +import CONSTANTS from './constants.json'; +import {isApnGetTagDefined, isGptPubadsDefined, logError, logWarn} from './utils.js'; import {auctionManager} from './auctionManager.js'; import {find, includes} from './polyfill.js'; -import {executeRenderer, isRendererRequired} from './Renderer.js'; -import {config} from './config.js'; -import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; +import {handleCreativeEvent, handleNativeMessage, handleRender} from './adRendering.js'; +import {getCreativeRendererSource} from './creativeRenderers.js'; -const BID_WON = constants.EVENTS.BID_WON; -const STALE_RENDER = constants.EVENTS.STALE_RENDER; -const WON_AD_IDS = new WeakSet(); +const {REQUEST, RESPONSE, NATIVE, EVENT} = CONSTANTS.MESSAGES; + +const BID_WON = CONSTANTS.EVENTS.BID_WON; const HANDLER_MAP = { - 'Prebid Request': handleRenderRequest, - 'Prebid Event': handleEventRequest, -} + [REQUEST]: handleRenderRequest, + [EVENT]: handleEventRequest, +}; if (FEATURES.NATIVE) { Object.assign(HANDLER_MAP, { - 'Prebid Native': handleNativeRequest, - }) + [NATIVE]: handleNativeRequest, + }); } export function listenMessagesFromCreative() { @@ -35,18 +34,18 @@ export function listenMessagesFromCreative() { export function getReplier(ev) { if (ev.origin == null && ev.ports.length === 0) { return function () { - const msg = 'Cannot post message to a frame with null origin. Please update creatives to use MessageChannel, see https://github.com/prebid/Prebid.js/issues/7870' - logError(msg) + const msg = 'Cannot post message to a frame with null origin. Please update creatives to use MessageChannel, see https://github.com/prebid/Prebid.js/issues/7870'; + logError(msg); throw new Error(msg); - } + }; } else if (ev.ports.length > 0) { return function (message) { ev.ports[0].postMessage(JSON.stringify(message)); - } + }; } else { return function (message) { ev.source.postMessage(JSON.stringify(message), ev.origin); - } + }; } } @@ -69,39 +68,24 @@ export function receiveMessage(ev) { } } -function handleRenderRequest(reply, data, adObject) { - if (adObject == null) { - emitAdRenderFail({ - reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - message: `Cannot find ad for cross-origin render request: '${data.adId}'`, - id: data.adId - }); - return; - } - if (adObject.status === constants.BID_STATUS.RENDERED) { - logWarn(`Ad id ${adObject.adId} has been rendered before`); - events.emit(STALE_RENDER, adObject); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } +function getResizer(bidResponse) { + return function (width, height) { + resizeRemoteCreative({...bidResponse, width, height}); } - - try { - _sendAdToCreative(adObject, reply); - } catch (e) { - emitAdRenderFail({ - reason: constants.AD_RENDER_FAILED_REASON.EXCEPTION, - message: e.message, - id: data.adId, - bid: adObject - }); - return; - } - - // save winning bids - auctionManager.addWinningBid(adObject); - - events.emit(BID_WON, adObject); +} +function handleRenderRequest(reply, message, bidResponse) { + handleRender({ + renderFn(adData) { + reply(Object.assign({ + message: RESPONSE, + renderer: getCreativeRendererSource(bidResponse) + }, adData)); + }, + resizeFn: getResizer(bidResponse), + options: message.options, + adId: message.adId, + bidResponse + }); } function handleNativeRequest(reply, data, adObject) { @@ -115,8 +99,7 @@ function handleNativeRequest(reply, data, adObject) { return; } - if (!WON_AD_IDS.has(adObject)) { - WON_AD_IDS.add(adObject); + if (adObject.status !== CONSTANTS.BID_STATUS.RENDERED) { auctionManager.addWinningBid(adObject); events.emit(BID_WON, adObject); } @@ -128,13 +111,8 @@ function handleNativeRequest(reply, data, adObject) { case 'allAssetRequest': reply(getAllAssetsMessage(data, adObject)); break; - case 'resizeNativeHeight': - adObject.height = data.height; - adObject.width = data.width; - resizeRemoteCreative(adObject); - break; default: - fireNativeTrackers(data, adObject); + handleNativeMessage(data, adObject, {resizeFn: getResizer(adObject)}) } } @@ -143,58 +121,25 @@ function handleEventRequest(reply, data, adObject) { logError(`Cannot find ad '${data.adId}' for x-origin event request`); return; } - if (adObject.status !== constants.BID_STATUS.RENDERED) { - logWarn(`Received x-origin event request without corresponding render request for ad '${data.adId}'`); + if (adObject.status !== CONSTANTS.BID_STATUS.RENDERED) { + logWarn(`Received x-origin event request without corresponding render request for ad '${adObject.adId}'`); return; } - switch (data.event) { - case constants.EVENTS.AD_RENDER_FAILED: - emitAdRenderFail({ - bid: adObject, - id: data.adId, - reason: data.info.reason, - message: data.info.message - }); - break; - case constants.EVENTS.AD_RENDER_SUCCEEDED: - emitAdRenderSucceeded({ - doc: null, - bid: adObject, - id: data.adId - }); - break; - default: - logError(`Received x-origin event request for unsupported event: '${data.event}' (adId: '${data.adId}')`) - } + return handleCreativeEvent(data, adObject); } -export function _sendAdToCreative(adObject, reply) { - const { adId, ad, adUrl, width, height, renderer, cpm, originalCpm } = adObject; - // rendering for outstream safeframe - if (isRendererRequired(renderer)) { - executeRenderer(renderer, adObject); - } else if (adId) { - resizeRemoteCreative(adObject); - reply({ - message: 'Prebid Response', - ad: replaceAuctionPrice(ad, originalCpm || cpm), - adUrl: replaceAuctionPrice(adUrl, originalCpm || cpm), - adId, - width, - height - }); +export function resizeRemoteCreative({adId, adUnitCode, width, height}) { + function getDimension(value) { + return value ? value + 'px' : '100%'; } -} - -function resizeRemoteCreative({ adId, adUnitCode, width, height }) { // resize both container div + iframe ['div', 'iframe'].forEach(elmType => { // not select element that gets removed after dfp render let element = getElementByAdUnit(elmType + ':not([style*="display: none"])'); if (element) { let elementStyle = element.style; - elementStyle.width = width ? width + 'px' : '100%'; - elementStyle.height = height + 'px'; + elementStyle.width = getDimension(width) + elementStyle.height = getDimension(height); } else { logWarn(`Unable to locate matching page element for adUnitCode ${adUnitCode}. Can't resize it to ad's dimensions. Please review setup.`); } @@ -208,9 +153,9 @@ function resizeRemoteCreative({ adId, adUnitCode, width, height }) { function getElementIdBasedOnAdServer(adId, adUnitCode) { if (isGptPubadsDefined()) { - return getDfpElementId(adId) + return getDfpElementId(adId); } else if (isApnGetTagDefined()) { - return getAstElementId(adUnitCode) + return getAstElementId(adUnitCode); } else { return adUnitCode; } diff --git a/src/targeting.js b/src/targeting.js index a75c9a2b52f..ddbc3cebaf3 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,8 +1,6 @@ import { deepAccess, deepClone, - getHighestCpm, - getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, isArray, @@ -24,19 +22,12 @@ import {hook} from './hook.js'; import {bidderSettings} from './bidderSettings.js'; import {find, includes} from './polyfill.js'; import CONSTANTS from './constants.json'; +import {getHighestCpm, getOldestHighestCpmBid} from './utils/reducers.js'; +import {getTTL} from './bidTTL.js'; var pbTargetingKeys = []; const MAX_DFP_KEYLENGTH = 20; -let DEFAULT_TTL_BUFFER = 1; - -config.getConfig('ttlBuffer', (cfg) => { - if (typeof cfg.ttlBuffer === 'number') { - DEFAULT_TTL_BUFFER = cfg.ttlBuffer; - } else { - logError('Invalid value for ttlBuffer', cfg.ttlBuffer); - } -}) const CFG_ALLOW_TARGETING_KEYS = `targetingControls.allowTargetingKeys`; const CFG_ADD_TARGETING_KEYS = `targetingControls.addTargetingKeys`; @@ -47,7 +38,7 @@ export const TARGETING_KEYS = Object.keys(CONSTANTS.TARGETING_KEYS).map( ); // return unexpired bids -const isBidNotExpired = (bid) => (bid.responseTimestamp + (bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : DEFAULT_TTL_BUFFER)) * 1000) > timestamp(); +const isBidNotExpired = (bid) => (bid.responseTimestamp + getTTL(bid) * 1000) > timestamp(); // return bids whose status is not set. Winning bids can only have a status of `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([CONSTANTS.BID_STATUS.RENDERED], bid.status)) || !bid.status); @@ -94,26 +85,26 @@ export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, }); /** -* A descending sort function that will sort the list of objects based on the following two dimensions: -* - bids with a deal are sorted before bids w/o a deal -* - then sort bids in each grouping based on the hb_pb value -* eg: the following list of bids would be sorted like: -* [{ -* "hb_adid": "vwx", -* "hb_pb": "28", -* "hb_deal": "7747" -* }, { -* "hb_adid": "jkl", -* "hb_pb": "10", -* "hb_deal": "9234" -* }, { -* "hb_adid": "stu", -* "hb_pb": "50" -* }, { -* "hb_adid": "def", -* "hb_pb": "2" -* }] -*/ + * A descending sort function that will sort the list of objects based on the following two dimensions: + * - bids with a deal are sorted before bids w/o a deal + * - then sort bids in each grouping based on the hb_pb value + * eg: the following list of bids would be sorted like: + * [{ + * "hb_adid": "vwx", + * "hb_pb": "28", + * "hb_deal": "7747" + * }, { + * "hb_adid": "jkl", + * "hb_pb": "10", + * "hb_deal": "9234" + * }, { + * "hb_adid": "stu", + * "hb_pb": "50" + * }, { + * "hb_adid": "def", + * "hb_pb": "2" + * }] + */ export function sortByDealAndPriceBucketOrCpm(useCpm = false) { return function(a, b) { if (a.adserverTargeting.hb_deal !== undefined && b.adserverTargeting.hb_deal === undefined) { @@ -479,6 +470,12 @@ export function newTargeting(auctionManager) { .filter(bid => deepAccess(bid, 'video.context') !== ADPOD) .filter(isBidUsable); + bidsReceived + .forEach(bid => { + bid.latestTargetedAuctionId = latestAuctionForAdUnit[bid.adUnitCode]; + return bid; + }); + return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); } @@ -500,7 +497,7 @@ export function newTargeting(auctionManager) { }; /** - * @param {(string|string[])} adUnitCode adUnitCode or array of adUnitCodes + * @param {(string|string[])} adUnitCodes adUnitCode or array of adUnitCodes * Sets targeting for AST */ targeting.setTargetingForAst = function(adUnitCodes) { @@ -533,7 +530,7 @@ export function newTargeting(auctionManager) { /** * Get targeting key value pairs for winning bid. - * @param {string[]} AdUnit code array + * @param {string[]} adUnitCodes code array * @return {targetingArray} winning bids targeting */ function getWinningBidTargeting(adUnitCodes, bidsReceived) { @@ -633,7 +630,7 @@ export function newTargeting(auctionManager) { /** * Get custom targeting key value pairs for bids. - * @param {string[]} AdUnit code array + * @param {string[]} adUnitCodes code array * @return {targetingArray} bids with custom targeting defined in bidderSettings */ function getCustomBidTargeting(adUnitCodes, bidsReceived) { @@ -647,7 +644,7 @@ export function newTargeting(auctionManager) { /** * Get targeting key value pairs for non-winning bids. - * @param {string[]} AdUnit code array + * @param {string[]} adUnitCodes code array * @return {targetingArray} all non-winning bids targeting */ function getBidLandscapeTargeting(adUnitCodes, bidsReceived) { diff --git a/src/types/ortb2.d.ts b/src/types/ortb2.d.ts new file mode 100644 index 00000000000..f38545c0c31 --- /dev/null +++ b/src/types/ortb2.d.ts @@ -0,0 +1,59 @@ +/** + * @see https://iabtechlab.com/standards/openrtb/ + */ +export namespace Ortb2 { + type Site = { + page?: string; + ref?: string; + domain?: string; + publisher?: { + domain?: string; + }; + keywords?: string; + ext?: Record; + }; + + type Device = { + w?: number; + h?: number; + dnt?: 0 | 1; + ua?: string; + language?: string; + sua?: { + source?: number; + platform?: unknown; + browsers?: unknown[]; + mobile?: number; + }; + ext?: { + webdriver?: true; + [key: string]: unknown; + }; + }; + + type Regs = { + coppa?: unknown; + ext?: { + gdpr?: unknown; + us_privacy?: unknown; + [key: string]: unknown; + }; + }; + + type User = { + ext?: Record; + }; + + /** + * Ortb2 info provided in bidder request. Some of the sections are mutually exclusive. + * @see clientSectionChecker + */ + type BidRequest = { + device?: Device; + regs?: Regs; + user?: User; + site?: Site; + app?: unknown; + dooh?: unknown; + }; +} diff --git a/src/userSync.js b/src/userSync.js index 936836eb12e..1b684de6de0 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -182,8 +182,8 @@ export function newUserSync(deps) { * @function incrementAdapterBids * @summary Increment the count of user syncs queue for the adapter * @private - * @params {object} numAdapterBids The object contain counts for all adapters - * @params {string} bidder The name of the bidder adding a sync + * @param {object} numAdapterBids The object contain counts for all adapters + * @param {string} bidder The name of the bidder adding a sync * @returns {object} The updated version of numAdapterBids */ function incrementAdapterBids(numAdapterBids, bidder) { @@ -199,10 +199,9 @@ export function newUserSync(deps) { * @function registerSync * @summary Add sync for this bidder to a queue to be fired later * @public - * @params {string} type The type of the sync including image, iframe - * @params {string} bidder The name of the adapter. e.g. "rubicon" - * @params {string} url Either the pixel url or iframe url depending on the type - + * @param {string} type The type of the sync including image, iframe + * @param {string} bidder The name of the adapter. e.g. "rubicon" + * @param {string} url Either the pixel url or iframe url depending on the type * @example Using Image Sync * // registerSync(type, adapter, pixelUrl) * userSync.registerSync('image', 'rubicon', 'http://example.com/pixel') @@ -244,7 +243,7 @@ export function newUserSync(deps) { * @param {string} type The type of the sync; either image or iframe * @param {string} bidder The name of the adapter. e.g. "rubicon" * @returns {boolean} true => bidder is not allowed to register; false => bidder can register - */ + */ function shouldBidderBeBlocked(type, bidder) { let filterConfig = usConfig.filterSettings; @@ -309,7 +308,7 @@ export function newUserSync(deps) { * @function syncUsers * @summary Trigger all the user syncs based on publisher-defined timeout * @public - * @params {int} timeout The delay in ms before syncing data - default 0 + * @param {number} timeout The delay in ms before syncing data - default 0 */ publicApi.syncUsers = (timeout = 0) => { if (timeout) { @@ -358,7 +357,7 @@ export const userSync = newUserSync(Object.defineProperties({ * * @property {boolean} enableOverride * @property {boolean} syncEnabled - * @property {int} syncsPerBidder + * @property {number} syncsPerBidder * @property {string[]} enabledBidders * @property {Object} filterSettings */ diff --git a/src/utils.js b/src/utils.js index ece29732723..c7ce5f22f9a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ import {config} from './config.js'; import clone from 'just-clone'; -import {find, includes} from './polyfill.js'; +import {includes} from './polyfill.js'; import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; import {getGlobal} from './prebidGlobal.js'; @@ -8,7 +8,6 @@ import {getGlobal} from './prebidGlobal.js'; export { default as deepAccess } from 'dlv/index.js'; export { dset as deepSetValue } from 'dset'; -var tArr = 'Array'; var tStr = 'String'; var tFn = 'Function'; var tNumb = 'Number'; @@ -64,17 +63,6 @@ export function getPrebidInternal() { return prebidInternal; } -var uniqueRef = {}; -export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === uniqueRef - ? Function.prototype.bind - : function(bind) { - var self = this; - var args = Array.prototype.slice.call(arguments, 1); - return function() { - return self.apply(bind, args.concat(Array.prototype.slice.call(arguments))); - }; - }; - /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; @@ -114,19 +102,7 @@ function _getRandomData() { } export function getBidIdParameter(key, paramsObj) { - if (paramsObj && paramsObj[key]) { - return paramsObj[key]; - } - - return ''; -} - -export function tryAppendQueryString(existingUrl, key, value) { - if (value) { - return existingUrl + key + '=' + encodeURIComponent(value) + '&'; - } - - return existingUrl; + return paramsObj?.[key] || ''; } // parse a query string object passed in bid params @@ -145,84 +121,30 @@ export function parseQueryStringParameters(queryObj) { export function transformAdServerTargetingObj(targeting) { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { - return getKeys(targeting) - .map(key => `${key}=${encodeURIComponent(getValue(targeting, key))}`).join('&'); + return Object.keys(targeting) + .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&'); } else { return ''; } } -/** - * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) - * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` - * @param {object} adUnit one adUnit object from the normal list of adUnits - * @returns {Array.} array of arrays containing numeric sizes - */ -export function getAdUnitSizes(adUnit) { - if (!adUnit) { - return; - } - - let sizes = []; - if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { - let bannerSizes = adUnit.mediaTypes.banner.sizes; - if (Array.isArray(bannerSizes[0])) { - sizes = bannerSizes; - } else { - sizes.push(bannerSizes); - } - // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders - } else if (Array.isArray(adUnit.sizes)) { - if (Array.isArray(adUnit.sizes[0])) { - sizes = adUnit.sizes; - } else { - sizes.push(adUnit.sizes); - } - } - return sizes; -} - /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ export function parseSizesInput(sizeObj) { - var parsedSizes = []; - - // if a string for now we can assume it is a single size, like "300x250" if (typeof sizeObj === 'string') { // multiple sizes will be comma-separated - var sizes = sizeObj.split(','); - - // regular expression to match strigns like 300x250 - // start of line, at least 1 number, an "x" , then at least 1 number, and the then end of the line - var sizeRegex = /^(\d)+x(\d)+$/i; - if (sizes) { - for (var curSizePos in sizes) { - if (hasOwn(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { - parsedSizes.push(sizes[curSizePos]); - } - } - } + return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i)) } else if (typeof sizeObj === 'object') { - var sizeArrayLength = sizeObj.length; - - // don't process empty array - if (sizeArrayLength > 0) { - // if we are a 2 item array of 2 numbers, we must be a SingleSize array - if (sizeArrayLength === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj)); - } else { - // otherwise, we must be a MultiSize array - for (var i = 0; i < sizeArrayLength; i++) { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj[i])); - } - } + if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { + return [parseGPTSingleSizeArray(sizeObj)]; + } else { + return sizeObj.map(parseGPTSingleSizeArray) } } - - return parsedSizes; + return []; } // Parse a GPT style single size array, (i.e [300, 250]) @@ -330,22 +252,37 @@ export function debugTurnedOn() { return !!config.getConfig('debug'); } +export const createIframe = (() => { + const DEFAULTS = { + border: '0px', + hspace: '0', + vspace: '0', + marginWidth: '0', + marginHeight: '0', + scrolling: 'no', + frameBorder: '0', + allowtransparency: 'true' + } + return (doc, attrs, style = {}) => { + const f = doc.createElement('iframe'); + Object.assign(f, Object.assign({}, DEFAULTS, attrs)); + Object.assign(f.style, style); + return f; + } +})(); + export function createInvisibleIframe() { - var f = document.createElement('iframe'); - f.id = getUniqueIdentifierStr(); - f.height = 0; - f.width = 0; - f.border = '0px'; - f.hspace = '0'; - f.vspace = '0'; - f.marginWidth = '0'; - f.marginHeight = '0'; - f.style.border = '0'; - f.scrolling = 'no'; - f.frameBorder = '0'; - f.src = 'about:blank'; - f.style.display = 'none'; - return f; + return createIframe(document, { + id: getUniqueIdentifierStr(), + width: 0, + height: 0, + src: 'about:blank' + }, { + display: 'none', + height: '0px', + width: '0px', + border: '0px' + }); } /* @@ -375,9 +312,7 @@ export function isStr(object) { return isA(object, tStr); } -export function isArray(object) { - return isA(object, tArr); -} +export const isArray = Array.isArray.bind(Array); export function isNumber(object) { return isA(object, tNumb); @@ -402,12 +337,7 @@ export function isEmpty(object) { if (isArray(object) || isStr(object)) { return !(object.length > 0); } - - for (var k in object) { - if (hasOwnProperty.call(object, k)) return false; - } - - return true; + return Object.keys(object).length <= 0; } /** @@ -426,38 +356,12 @@ export function isEmptyStr(str) { * @param {Function(value, key, object)} fn */ export function _each(object, fn) { - if (isEmpty(object)) return; - if (isFn(object.forEach)) return object.forEach(fn, this); - - var k = 0; - var l = object.length; - - if (l > 0) { - for (; k < l; k++) fn(object[k], k, object); - } else { - for (k in object) { - if (hasOwnProperty.call(object, k)) fn.call(this, object[k], k); - } - } + if (isFn(object?.forEach)) return object.forEach(fn, this); + Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } export function contains(a, obj) { - if (isEmpty(a)) { - return false; - } - - if (isFn(a.indexOf)) { - return a.indexOf(obj) !== -1; - } - - var i = a.length; - while (i--) { - if (a[i] === obj) { - return true; - } - } - - return false; + return isFn(a?.includes) && a.includes(obj); } /** @@ -468,24 +372,10 @@ export function contains(a, obj) { * @return {Array} */ export function _map(object, callback) { - if (isEmpty(object)) return []; - if (isFn(object.map)) return object.map(callback); - var output = []; - _each(object, function (value, key) { - output.push(callback(value, key, object)); - }); - - return output; + if (isFn(object?.map)) return object.map(callback); + return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)) } -export function hasOwn(objectToCheck, propertyToCheckFor) { - if (objectToCheck.hasOwnProperty) { - return objectToCheck.hasOwnProperty(propertyToCheckFor); - } else { - return (typeof objectToCheck[propertyToCheckFor] !== 'undefined') && (objectToCheck.constructor.prototype[propertyToCheckFor] !== objectToCheck[propertyToCheckFor]); - } -}; - /* * Inserts an element(elm) as targets child, by default as first child * @param {HTMLElement} elm @@ -568,27 +458,14 @@ export function insertHtmlIntoIframe(htmlCode) { if (!htmlCode) { return; } - - let iframe = document.createElement('iframe'); - iframe.id = getUniqueIdentifierStr(); - iframe.width = 0; - iframe.height = 0; - iframe.hspace = '0'; - iframe.vspace = '0'; - iframe.marginWidth = '0'; - iframe.marginHeight = '0'; - iframe.style.display = 'none'; - iframe.style.height = '0px'; - iframe.style.width = '0px'; - iframe.scrolling = 'no'; - iframe.frameBorder = '0'; - iframe.allowtransparency = 'true'; - + const iframe = createInvisibleIframe(); internal.insertElement(iframe, document, 'body'); - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(htmlCode); - iframe.contentWindow.document.close(); + ((doc) => { + doc.open(); + doc.write(htmlCode); + doc.close(); + })(iframe.contentWindow.document); } /** @@ -654,19 +531,6 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -export function getValueString(param, val, defaultValue) { - if (val === undefined || val === null) { - return defaultValue; - } - if (isStr(val)) { - return val; - } - if (isNumber(val)) { - return val.toString(); - } - internal.logWarn('Unsuported type for param: ' + param + ' required type: String'); -} - export function uniques(value, index, arry) { return arry.indexOf(value) === index; } @@ -679,38 +543,14 @@ export function getBidRequest(id, bidderRequests) { if (!id) { return; } - let bidRequest; - bidderRequests.some(bidderRequest => { - let result = find(bidderRequest.bids, bid => ['bidId', 'adId', 'bid_id'].some(type => bid[type] === id)); - if (result) { - bidRequest = result; - } - return result; - }); - return bidRequest; -} - -export function getKeys(obj) { - return Object.keys(obj); + return bidderRequests.flatMap(br => br.bids) + .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)) } export function getValue(obj, key) { return obj[key]; } -/** - * Get the key of an object for a given value - */ -export function getKeyByValue(obj, value) { - for (let prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (obj[prop] === value) { - return prop; - } - } - } -} - export function getBidderCodes(adUnits = pbjsInstance.adUnits) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) @@ -729,26 +569,6 @@ export function isApnGetTagDefined() { } } -// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = getHighestCpmCallback('timeToRespond', (previous, current) => previous > current); - -// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 -export const getOldestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous > current); - -// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 -export const getLatestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous < current); - -function getHighestCpmCallback(useTieBreakerProperty, tieBreakerCallback) { - return (previous, current) => { - if (previous.cpm === current.cpm) { - return tieBreakerCallback(previous[useTieBreakerProperty], current[useTieBreakerProperty]) ? current : previous; - } - return previous.cpm < current.cpm ? current : previous; - } -} - /** * Fisher–Yates shuffle * http://stackoverflow.com/a/6274398 @@ -775,10 +595,6 @@ export function shuffle(array) { return array; } -export function adUnitsFilter(filter, bid) { - return includes(filter, bid && bid.adUnitCode); -} - export function deepClone(obj) { return clone(obj); } @@ -795,9 +611,15 @@ export function isSafariBrowser() { return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } -export function replaceAuctionPrice(str, cpm) { +export function replaceMacros(str, subs) { if (!str) return; - return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); + return Object.entries(subs).reduce((str, [key, val]) => { + return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || ''); + }, str); +} + +export function replaceAuctionPrice(str, cpm) { + return replaceMacros(str, {AUCTION_PRICE: cpm}) } export function replaceClickThrough(str, clicktag) { @@ -843,7 +665,7 @@ export function checkCookieSupport() { * * @param {function} func The function which should be executed, once the returned function has been executed * numRequiredCalls times. - * @param {int} numRequiredCalls The number of times which the returned function needs to be called before + * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ export function delayExecution(func, numRequiredCalls) { @@ -862,7 +684,7 @@ export function delayExecution(func, numRequiredCalls) { /** * https://stackoverflow.com/a/34890276/428704 * @export - * @param {array} xs + * @param {Array} xs * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ @@ -925,8 +747,7 @@ export function isValidMediaTypes(mediaTypes) { export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { return adUnits .filter(adUnit => adUnit.code === adUnitCode) - .map((adUnit) => adUnit.bids) - .reduce(flatten, []) + .flatMap((adUnit) => adUnit.bids) .filter((bidderData) => bidderData.bidder === bidder) .map((bidderData) => bidderData.params || {}); } @@ -938,7 +759,7 @@ export function getDNT() { return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes'; } -const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; +export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot @@ -949,41 +770,6 @@ export function isAdUnitCodeMatchingSlot(slot) { return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); } -/** - * Returns filter function to match adUnitCode in slot - * @param {string} adUnitCode AdUnit code - * @return {function} filter function - */ -export function isSlotMatchingAdUnitCode(adUnitCode) { - return (slot) => compareCodeAndSlot(slot, adUnitCode); -} - -/** - * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page - */ -export function getGptSlotForAdUnitCode(adUnitCode) { - let matchingSlot; - if (isGptPubadsDefined()) { - // find the first matching gpt slot on the page - matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); - } - return matchingSlot; -}; - -/** - * @summary Uses the adUnit's code in order to find a matching gptSlot on the page - */ -export function getGptSlotInfoForAdUnitCode(adUnitCode) { - const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); - if (matchingSlot) { - return { - gptSlot: matchingSlot.getAdUnitPath(), - divId: matchingSlot.getSlotElementId() - } - } - return {}; -}; - /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped @@ -1005,33 +791,14 @@ export function unsupportedBidderMessage(adUnit, bidder) { * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger * @param {*} value */ -export function isInteger(value) { - if (Number.isInteger) { - return Number.isInteger(value); - } else { - return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; - } -} - -/** - * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' - * @param {string} value string value to convert - */ -export function convertCamelToUnderscore(value) { - return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { return '_' + y.toLowerCase() }).replace(/^_/, ''); -} +export const isInteger = Number.isInteger.bind(Number); /** * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ export function cleanObj(obj) { - return Object.keys(obj).reduce((newObj, key) => { - if (typeof obj[key] !== 'undefined') { - newObj[key] = obj[key]; - } - return newObj; - }, {}) + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')) } /** @@ -1068,104 +835,10 @@ export function pick(obj, properties) { }, {}); } -/** - * Try to convert a value to a type. - * If it can't be done, the value will be returned. - * - * @param {string} typeToConvert The target type. e.g. "string", "number", etc. - * @param {*} value The value to be converted into typeToConvert. - */ -function tryConvertType(typeToConvert, value) { - if (typeToConvert === 'string') { - return value && value.toString(); - } else if (typeToConvert === 'number') { - return Number(value); - } else { - return value; - } -} - -export function convertTypes(types, params) { - Object.keys(types).forEach(key => { - if (params[key]) { - if (isFn(types[key])) { - params[key] = types[key](params[key]); - } else { - params[key] = tryConvertType(types[key], params[key]); - } - - // don't send invalid values - if (isNaN(params[key])) { - delete params.key; - } - } - }); - return params; -} - export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } -/** - * Creates an array of n length and fills each item with the given value - */ -export function fill(value, length) { - let newArray = []; - - for (let i = 0; i < length; i++) { - let valueToPush = isPlainObject(value) ? deepClone(value) : value; - newArray.push(valueToPush); - } - - return newArray; -} - -/** - * http://npm.im/chunk - * Returns an array with *size* chunks from given array - * - * Example: - * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => - * [['a', 'b'], ['c', 'd'], ['e']] - */ -export function chunk(array, size) { - let newArray = []; - - for (let i = 0; i < Math.ceil(array.length / size); i++) { - let start = i * size; - let end = start + size; - newArray.push(array.slice(start, end)); - } - - return newArray; -} - -export function getMinValueFromArray(array) { - return Math.min(...array); -} - -export function getMaxValueFromArray(array) { - return Math.max(...array); -} - -/** - * This function will create compare function to sort on object property - * @param {string} property - * @returns {function} compare function to be used in sorting - */ -export function compareOn(property) { - return function compare(a, b) { - if (a[property] < b[property]) { - return 1; - } - if (a[property] > b[property]) { - return -1; - } - return 0; - } -} - export function parseQS(query) { return !query ? {} : query .replace(/^\?/, '') @@ -1237,8 +910,9 @@ export function deepEqual(obj1, obj2, {checkTypes = false} = {}) { (typeof obj2 === 'object' && obj2 !== null) && (!checkTypes || (obj1.constructor === obj2.constructor)) ) { - if (Object.keys(obj1).length !== Object.keys(obj2).length) return false; - for (let prop in obj1) { + const props1 = Object.keys(obj1); + if (props1.length !== Object.keys(obj2).length) return false; + for (let prop of props1) { if (obj2.hasOwnProperty(prop)) { if (!deepEqual(obj1[prop], obj2[prop], {checkTypes})) { return false; @@ -1328,15 +1002,6 @@ export function cyrb53Hash(str, seed = 0) { return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); } -/** - * returns a window object, which holds the provided document or null - * @param {Document} doc - * @returns {Window} - */ -export function getWindowFromDocument(doc) { - return (doc) ? doc.defaultView : null; -} - /** * returns the result of `JSON.parse(data)`, or undefined if that throws an error. * @param data @@ -1375,36 +1040,33 @@ export function memoize(fn, key = function (arg) { return arg; }) { * @param {object} attributes */ export function setScriptAttributes(script, attributes) { - for (let key in attributes) { - if (attributes.hasOwnProperty(key)) { - script.setAttribute(key, attributes[key]); - } - } + Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)) } /** - * Encode a string for inclusion in HTML. - * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and - * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ - * @return {string} + * Perform a binary search for `el` on an ordered array `arr`. + * + * @returns the lowest nonnegative integer I that satisfies: + * key(arr[i]) >= key(el) for each i between I and arr.length + * + * (if one or more matches are found for `el`, returns the index of the first; + * if the element is not found, return the index of the first element that's greater; + * if no greater element exists, return `arr.length`) */ -export const escapeUnsafeChars = (() => { - const escapes = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', - '\0': '\\0', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - - return function(str) { - return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]) +export function binarySearch(arr, el, key = (el) => el) { + let left = 0; + let right = arr.length && arr.length - 1; + const target = key(el); + while (right - left > 1) { + const middle = left + Math.round((right - left) / 2); + if (target > key(arr[middle])) { + left = middle; + } else { + right = middle; + } } -})(); + while (arr.length > left && target > key(arr[left])) { + left++; + } + return left; +} diff --git a/src/utils/currency.js b/src/utils/currency.js deleted file mode 100644 index ab7e5faa1ea..00000000000 --- a/src/utils/currency.js +++ /dev/null @@ -1,16 +0,0 @@ -import {getGlobal} from '../prebidGlobal.js'; - -/** - * "best effort" wrapper around currency conversion; always returns an amount that may or may not be correct. - */ -export function beConvertCurrency(amount, from, to) { - if (from === to) return amount; - let result = amount; - if (typeof getGlobal().convertCurrency === 'function') { - try { - result = getGlobal().convertCurrency(amount, from, to); - } catch (e) { - } - } - return result; -} diff --git a/src/utils/reducers.js b/src/utils/reducers.js new file mode 100644 index 00000000000..28851be8aaa --- /dev/null +++ b/src/utils/reducers.js @@ -0,0 +1,44 @@ +export function simpleCompare(a, b) { + if (a === b) return 0; + return a < b ? -1 : 1; +} + +export function keyCompare(key = (item) => item) { + return (a, b) => simpleCompare(key(a), key(b)) +} + +export function reverseCompare(compare = simpleCompare) { + return (a, b) => -compare(a, b) || 0; +} + +export function tiebreakCompare(...compares) { + return function (a, b) { + for (const cmp of compares) { + const val = cmp(a, b); + if (val !== 0) return val; + } + return 0; + } +} + +export function minimum(compare = simpleCompare) { + return (min, item) => compare(item, min) < 0 ? item : min; +} + +export function maximum(compare = simpleCompare) { + return minimum(reverseCompare(compare)); +} + +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); + +// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))) + +// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 +export const getOldestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, reverseCompare(timestampCompare))) + +// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 +export const getLatestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, timestampCompare)) diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js new file mode 100644 index 00000000000..0972d175848 --- /dev/null +++ b/src/utils/ttlCollection.js @@ -0,0 +1,162 @@ +import {GreedyPromise} from './promise.js'; +import {binarySearch, logError, timestamp} from '../utils.js'; + +/** + * Create a set-like collection that automatically forgets items after a certain time. + * + * @param {({}) => Number|Promise} startTime? a function taking an item added to this collection, + * and returning (a promise to) a timestamp to be used as the starting time for the item + * (the item will be dropped after `ttl(item)` milliseconds have elapsed since this timestamp). + * Defaults to the time the item was added to the collection. + * @param {({}) => Number|void|Promise} ttl a function taking an item added to this collection, + * and returning (a promise to) the duration (in milliseconds) the item should be kept in it. + * May return null to indicate that the item should be persisted indefinitely. + * @param {boolean} monotonic? set to true for better performance, but only if, given any two items A and B in this collection: + * if A was added before B, then: + * - startTime(A) + ttl(A) <= startTime(B) + ttl(B) + * - Promise.all([startTime(A), ttl(A)]) never resolves later than Promise.all([startTime(B), ttl(B)]) + * @param {number} slack? maximum duration (in milliseconds) that an item is allowed to persist + * once past its TTL. This is also roughly the interval between "garbage collection" sweeps. + */ +export function ttlCollection( + { + startTime = timestamp, + ttl = () => null, + monotonic = false, + slack = 5000 + } = {} +) { + const items = new Map(); + const callbacks = []; + const pendingPurge = []; + const markForPurge = monotonic + ? (entry) => pendingPurge.push(entry) + : (entry) => pendingPurge.splice(binarySearch(pendingPurge, entry, (el) => el.expiry), 0, entry) + let nextPurge, task; + + function reschedulePurge() { + task && clearTimeout(task); + if (pendingPurge.length > 0) { + const now = timestamp(); + nextPurge = Math.max(now, pendingPurge[0].expiry + slack); + task = setTimeout(() => { + const now = timestamp(); + let cnt = 0; + for (const entry of pendingPurge) { + if (entry.expiry > now) break; + callbacks.forEach(cb => { + try { + cb(entry.item) + } catch (e) { + logError(e); + } + }); + items.delete(entry.item) + cnt++; + } + pendingPurge.splice(0, cnt); + task = null; + reschedulePurge(); + }, nextPurge - now); + } else { + task = null; + } + } + + function mkEntry(item) { + const values = {}; + const thisCohort = currentCohort; + let expiry; + + function update() { + if (thisCohort === currentCohort && values.start != null && values.delta != null) { + expiry = values.start + values.delta; + markForPurge(entry); + if (task == null || nextPurge > expiry + slack) { + reschedulePurge(); + } + } + } + + const [init, refresh] = Object.entries({ + start: startTime, + delta: ttl + }).map(([field, getter]) => { + let currentCall; + return function() { + const thisCall = currentCall = {}; + GreedyPromise.resolve(getter(item)).then((val) => { + if (thisCall === currentCall) { + values[field] = val; + update(); + } + }); + } + }) + + const entry = { + item, + refresh, + get expiry() { + return expiry; + }, + }; + + init(); + refresh(); + return entry; + } + + let currentCohort = {}; + + return { + [Symbol.iterator]: () => items.keys(), + /** + * Add an item to this collection. + * @param item + */ + add(item) { + !items.has(item) && items.set(item, mkEntry(item)); + }, + /** + * Clear this collection. + */ + clear() { + pendingPurge.length = 0; + reschedulePurge(); + items.clear(); + currentCohort = {}; + }, + /** + * @returns {[]} all the items in this collection, in insertion order. + */ + toArray() { + return Array.from(items.keys()); + }, + /** + * Refresh the TTL for each item in this collection. + */ + refresh() { + pendingPurge.length = 0; + reschedulePurge(); + for (const entry of items.values()) { + entry.refresh(); + } + }, + /** + * Register a callback to be run when an item has expired and is about to be + * removed the from the collection. + * @param cb a callback that takes the expired item as argument + * @return an unregistration function. + */ + onExpiry(cb) { + callbacks.push(cb); + return () => { + const idx = callbacks.indexOf(cb); + if (idx >= 0) { + callbacks.splice(idx, 1); + } + } + } + }; +} diff --git a/src/video.js b/src/video.js index 7930e318874..ff137892a2b 100644 --- a/src/video.js +++ b/src/video.js @@ -1,25 +1,21 @@ -import adapterManager from './adapterManager.js'; -import { deepAccess, logError } from './utils.js'; -import { config } from '../src/config.js'; -import {includes} from './polyfill.js'; -import { hook } from './hook.js'; +import {deepAccess, logError} from './utils.js'; +import {config} from '../src/config.js'; +import {hook} from './hook.js'; import {auctionManager} from './auctionManager.js'; -const VIDEO_MEDIA_TYPE = 'video'; export const OUTSTREAM = 'outstream'; export const INSTREAM = 'instream'; -/** - * Helper functions for working with video-enabled adUnits - */ -export const videoAdUnit = adUnit => { - const mediaType = adUnit.mediaType === VIDEO_MEDIA_TYPE; - const mediaTypes = deepAccess(adUnit, 'mediaTypes.video'); - return mediaType || mediaTypes; -}; -export const videoBidder = bid => includes(adapterManager.videoAdapters, bid.bidder); -export const hasNonVideoBidder = adUnit => - adUnit.bids.filter(bid => !videoBidder(bid)).length; +export function fillVideoDefaults(adUnit) { + const video = adUnit?.mediaTypes?.video; + if (video != null && video.plcmt == null) { + if (video.context === OUTSTREAM || [2, 3, 4].includes(video.placement)) { + video.plcmt = 4; + } else if (video.context !== OUTSTREAM && [2, 6].includes(video.playbackmethod)) { + video.plcmt = 2; + } + } +} /** * @typedef {object} VideoBid diff --git a/src/videoCache.js b/src/videoCache.js index 88fc27625fd..ce03f2f624e 100644 --- a/src/videoCache.js +++ b/src/videoCache.js @@ -42,17 +42,18 @@ const ttlBufferInSeconds = 15; * @param {string} impUrl An impression tracker URL for the delivery of the video ad * @return A VAST URL which loads XML from the given URI. */ -function wrapURI(uri, impUrl) { +function wrapURI(uri, impTrackerURLs) { + impTrackerURLs = impTrackerURLs && (Array.isArray(impTrackerURLs) ? impTrackerURLs : [impTrackerURLs]); // Technically, this is vulnerable to cross-script injection by sketchy vastUrl bids. // We could make sure it's a valid URI... but since we're loading VAST XML from the // URL they provide anyway, that's probably not a big deal. - let vastImp = (impUrl) ? `` : ``; + let impressions = impTrackerURLs ? impTrackerURLs.map(trk => ``).join('') : ''; return ` prebid.org wrapper - ${vastImp} + ${impressions} diff --git a/test/fake-server/fake-responder.js b/test/fake-server/fake-responder.js index a44d02260e7..13bf3bc816f 100644 --- a/test/fake-server/fake-responder.js +++ b/test/fake-server/fake-responder.js @@ -9,7 +9,7 @@ const fixturesPath = path.join(__dirname, 'fixtures'); /** * Matches 'req.body' with the responseBody pair * @param {object} requestBody - `req.body` of incoming request hitting middleware 'fakeResponder'. - * @returns {objct} responseBody + * @returns {object} responseBody */ const matchResponse = function (requestBody) { let actualUuids = []; diff --git a/test/fake-server/fixtures/basic-outstream/request.json b/test/fake-server/fixtures/basic-outstream/request.json index e9f3302ab4c..6d522058cff 100644 --- a/test/fake-server/fixtures/basic-outstream/request.json +++ b/test/fake-server/fixtures/basic-outstream/request.json @@ -19,6 +19,7 @@ "prebid": true, "disable_psa": true, "video": { + "context": 4, "skippable": true, "playback_method": 2 }, @@ -39,6 +40,7 @@ "prebid": true, "disable_psa": true, "video": { + "context": 4, "skippable": true, "playback_method": 2 }, diff --git a/test/helpers/indexStub.js b/test/helpers/indexStub.js index 2916b60ae32..5202106c9cf 100644 --- a/test/helpers/indexStub.js +++ b/test/helpers/indexStub.js @@ -1,6 +1,6 @@ import {AuctionIndex} from '../../src/auctionIndex.js'; -export function stubAuctionIndex({bidRequests, bidderRequests, adUnits}) { +export function stubAuctionIndex({bidRequests, bidderRequests, adUnits, auctionId = 'mock-auction'}) { if (adUnits == null) { adUnits = [] } @@ -15,7 +15,7 @@ export function stubAuctionIndex({bidRequests, bidderRequests, adUnits}) { } const auction = { getAuctionId() { - return 'mock-auction' + return auctionId; }, getBidRequests() { return bidderRequests; diff --git a/test/helpers/testing-utils.js b/test/helpers/testing-utils.js index 1336a90ecbf..3f59411ff6c 100644 --- a/test/helpers/testing-utils.js +++ b/test/helpers/testing-utils.js @@ -7,23 +7,23 @@ const utils = { testPageURL: function(name) { return `${utils.protocol}://${utils.host}:9999/test/pages/${name}` }, - waitForElement: function(elementRef, time = DEFAULT_TIMEOUT) { + waitForElement: async function(elementRef, time = DEFAULT_TIMEOUT) { let element = $(elementRef); - element.waitForExist({timeout: time}); + await element.waitForExist({timeout: time}); }, - switchFrame: function(frameRef) { - let iframe = $(frameRef); + switchFrame: async function(frameRef) { + let iframe = await $(frameRef); browser.switchToFrame(iframe); }, - loadAndWaitForElement(url, selector, pause = 3000, timeout = DEFAULT_TIMEOUT, retries = 3, attempt = 1) { - browser.url(url); - browser.pause(pause); + async loadAndWaitForElement(url, selector, pause = 3000, timeout = DEFAULT_TIMEOUT, retries = 3, attempt = 1) { + await browser.url(url); + await browser.pause(pause); if (selector != null) { try { - utils.waitForElement(selector, timeout); + await utils.waitForElement(selector, timeout); } catch (e) { if (attempt < retries) { - utils.loadAndWaitForElement(url, selector, pause, timeout, retries, attempt + 1); + await utils.loadAndWaitForElement(url, selector, pause, timeout, retries, attempt + 1); } } } @@ -35,14 +35,15 @@ const utils = { fn.call(this); if (expectGAMCreative) { expectGAMCreative = expectGAMCreative === true ? waitFor : expectGAMCreative; - it(`should render GAM creative`, () => { - utils.switchFrame(expectGAMCreative); + it(`should render GAM creative`, async () => { + await utils.switchFrame(expectGAMCreative); const creative = [ '> a > img', // banner '> div[class="card"]' // native ].map((child) => `body > div[class="GoogleActiveViewElement"] ${child}`) .join(', '); - expect($(creative).isExisting()).to.be.true; + const existing = await $(creative).isExisting(); + expect(existing).to.be.true; }); } }); diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index 424100f870c..e7b1d96f0a4 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -1,12 +1,236 @@ import {getUniqueIdentifierStr} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; +import {fakeXhr} from 'nise'; +import {dep} from 'src/ajax.js'; -export let server = sinon.createFakeServer(); -export let xhr = global.XMLHttpRequest; +export const xhr = sinon.useFakeXMLHttpRequest(); +export const server = mockFetchServer(); -beforeEach(function() { - server.restore(); - server = sinon.createFakeServer(); - xhr = global.XMLHttpRequest; +/** + * An (incomplete) replica of nise's fakeServer, but backing fetch used in ajax.js (rather than XHR). + */ +function mockFetchServer() { + const sandbox = sinon.createSandbox(); + const bodies = new WeakMap(); + const requests = []; + const {DONE, UNSENT} = XMLHttpRequest; + + function makeRequest(resource, options) { + const requestBody = options?.body || bodies.get(resource); + const request = new Request(resource, options); + bodies.set(request, requestBody); + return request; + } + + function mockXHR(resource, options) { + let resolve, reject; + const promise = new GreedyPromise((res, rej) => { + resolve = res; + reject = rej; + }); + + function error(reason = new TypeError('Failed to fetch')) { + mockReq.status = 0; + reject(reason); + } + + const request = makeRequest(resource, options); + request.signal.onabort = () => error(new DOMException('The user aborted a request')); + let responseHeaders; + + const mockReq = { + fetch: { + request, + requestBody: bodies.get(request), + promise, + }, + readyState: UNSENT, + url: request.url, + method: request.method, + requestBody: bodies.get(request), + status: 0, + statusText: '', + requestHeaders: new Proxy(request.headers, { + get(target, prop) { + return typeof prop === 'string' && target.has(prop) ? target.get(prop) : {}[prop]; + }, + has(target, prop) { + return typeof prop === 'string' && target.has(prop); + }, + ownKeys(target) { + return Array.from(target.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (typeof prop === 'string' && target.has(prop)) { + return { + enumerable: true, + configurable: true, + writable: false, + value: target.get(prop) + } + } + } + }), + withCredentials: request.credentials === 'include', + setStatus(status) { + // nise replaces invalid status with 200 + status = typeof status === 'number' ? status : 200; + mockReq.status = status; + mockReq.statusText = fakeXhr.FakeXMLHttpRequest.statusCodes[status] || ''; + }, + setResponseHeaders(headers) { + responseHeaders = headers; + }, + setResponseBody(body) { + if (mockReq.status === 0) { + error(); + return; + } + const resp = Object.defineProperties(new Response(body, { + status: mockReq.status, + statusText: mockReq.statusText, + headers: responseHeaders || {}, + }), { + url: { + get: () => mockReq.fetch.request.url, + } + }); + mockReq.readyState = DONE; + // tests expect respond() to run everything immediately, + // so make body available syncronously + resp.text = () => GreedyPromise.resolve(body || ''); + Object.assign(mockReq.fetch, { + response: resp, + responseBody: body || '' + }) + resolve(resp); + }, + respond(status = 200, headers, body) { + mockReq.setStatus(status); + mockReq.setResponseHeaders(headers); + mockReq.setResponseBody(body); + }, + error + }; + return mockReq; + } + + let enabled = false; + let timeoutsEnabled = false; + + function enable() { + if (!enabled) { + sandbox.stub(dep, 'fetch').callsFake((resource, options) => { + const req = mockXHR(resource, options); + requests.push(req); + return req.fetch.promise; + }); + sandbox.stub(dep, 'makeRequest').callsFake(makeRequest); + const timeout = dep.timeout; + sandbox.stub(dep, 'timeout').callsFake(function () { + if (timeoutsEnabled) { + return timeout.apply(null, arguments); + } else { + return {}; + } + }); + enabled = true; + } + } + + enable(); + + const responders = []; + + function respondWith() { + let response, urlMatcher, methodMatcher; + urlMatcher = methodMatcher = () => true; + switch (arguments.length) { + case 1: + ([response] = arguments); + break; + case 2: + ([urlMatcher, response] = arguments); + break; + case 3: + ([methodMatcher, urlMatcher, response] = arguments); + methodMatcher = ((toMatch) => (method) => method === toMatch)(methodMatcher); + break; + default: + throw new Error('Invalid respondWith invocation'); + } + if (typeof urlMatcher.exec === 'function') { + urlMatcher = ((rx) => (url) => rx.exec(url)?.slice(1))(urlMatcher); + } else if (typeof urlMatcher === 'string') { + urlMatcher = ((toMatch) => (url) => url === toMatch)(urlMatcher); + } + responders.push((req) => { + if (req.readyState !== DONE && methodMatcher(req.method)) { + const arg = urlMatcher(req.url); + if (arg) { + if (typeof response === 'function') { + response(req, ...(Array.isArray(arg) ? arg : [])); + } else if (typeof response === 'string') { + req.respond(200, null, response); + } else { + req.respond.apply(req, response); + } + } + } + }); + } + + function resetState() { + requests.length = 0; + responders.length = 0; + timeoutsEnabled = false; + } + + return { + requests, + enable, + restore() { + resetState(); + sandbox.restore(); + enabled = false; + }, + reset() { + sandbox.resetHistory(); + resetState(); + }, + respondWith, + respond() { + if (arguments.length > 0) { + respondWith.apply(null, arguments); + } + requests.forEach(req => { + for (let i = responders.length - 1; i >= 0; i--) { + responders[i](req); + if (req.readyState === DONE) break; + } + if (req.readyState !== DONE) { + req.respond(404, {}, ''); + } + }); + }, + /** + * the timeout mechanism is quite different between XHR and fetch + * by default, mocked fetch does not time out - to reflect fakeServer XHRs + * note that many tests will fire requests without caring or waiting for their response - + * if they are timed out later, during unrelated tests, the log messages might interfere with their + * assertions + */ + get autoTimeout() { + return timeoutsEnabled; + }, + set autoTimeout(val) { + timeoutsEnabled = !!val; + } + }; +} + +beforeEach(function () { + server.reset(); }); const bid = getUniqueIdentifierStr().substring(4); @@ -20,12 +244,35 @@ afterEach(function () { return (s) => s.split('\n').map(s => `${preamble} ${s}`).join('\n'); })(); + function format(obj, body = null) { + if (obj == null) return obj; + const fmt = {}; + let node = obj; + while (node != null) { + Object.keys(node).forEach((k) => { + const val = obj[k]; + if (typeof val !== 'function' && !fmt.hasOwnProperty(k)) { + fmt[k] = val; + } + }); + node = Object.getPrototypeOf(node); + } + if (obj.headers != null) { + fmt.headers = Object.fromEntries(obj.headers.entries()) + } + fmt.body = body; + return fmt; + } + - console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)) + console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)); server.requests.forEach((req, i) => { console.log(prepend(`Request #${i}:`)); - console.log(prepend(JSON.stringify(req, null, 2))); - }) + console.log(prepend(JSON.stringify({ + request: format(req.fetch.request, req.fetch.requestBody), + response: format(req.fetch.response, req.fetch.responseBody) + }, null, 2))); + }); } }); /* eslint-enable */ diff --git a/test/pages/consent_mgt_gdpr.html b/test/pages/consent_mgt_gdpr.html index b22d1e958e0..c55a2b9236f 100644 --- a/test/pages/consent_mgt_gdpr.html +++ b/test/pages/consent_mgt_gdpr.html @@ -150,7 +150,6 @@ consentManagement: { gdpr: { cmpApi: 'static', - allowAuctionWithoutConsent: true, consentData: { getConsentData: { 'gdprApplies': true, diff --git a/test/spec/appnexusKeywords_spec.js b/test/spec/appnexusKeywords_spec.js index 9bf567a27c5..68faeff0b82 100644 --- a/test/spec/appnexusKeywords_spec.js +++ b/test/spec/appnexusKeywords_spec.js @@ -1,4 +1,4 @@ -import {transformBidderParamKeywords} from '../../libraries/appnexusKeywords/anKeywords.js'; +import {transformBidderParamKeywords} from '../../libraries/appnexusUtils/anKeywords.js'; import {expect} from 'chai/index.js'; import * as utils from '../../src/utils.js'; diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 4061e757d97..06d3d538596 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -5,7 +5,7 @@ import { adjustBids, getMediaTypeGranularity, getPriceByGranularity, - addBidResponse + addBidResponse, resetAuctionState, responsesReady } from 'src/auction.js'; import CONSTANTS from 'src/constants.json'; import * as auctionModule from 'src/auction.js'; @@ -23,6 +23,12 @@ import {AuctionIndex} from '../../src/auctionIndex.js'; import {expect} from 'chai'; import {deepClone} from '../../src/utils.js'; import { IMAGE as ortbNativeRequest } from 'src/native.js'; +import {PrebidServer} from '../../modules/prebidServerBidAdapter/index.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ var assert = require('assert'); @@ -48,6 +54,7 @@ function mockBid(opts) { let bidderCode = opts && opts.bidderCode; return { + adUnitCode: opts?.adUnitCode || ADUNIT_CODE, 'ad': 'creative', 'cpm': '1.99', 'width': 300, @@ -55,6 +62,7 @@ function mockBid(opts) { 'bidderCode': bidderCode || BIDDER_CODE, 'requestId': utils.getUniqueIdentifierStr(), 'transactionId': (opts && opts.transactionId) || ADUNIT_CODE, + adUnitId: (opts && opts.adUnitId) || ADUNIT_CODE, 'creativeId': 'id', 'currency': 'USD', 'netRevenue': true, @@ -68,6 +76,11 @@ function mockBid(opts) { transactionId: this.transactionId, auctionId: this.auctionId } + }, + _ctx: { + adUnits: opts?.adUnits, + src: opts?.src, + uniquePbsTid: opts?.uniquePbsTid, } }; } @@ -96,6 +109,9 @@ function mockBidRequest(bid, opts) { 'bidderCode': bidderCode || bid.bidderCode, 'auctionId': opts && opts.auctionId, 'bidderRequestId': requestId, + src: bid?._ctx?.src, + adUnitsS2SCopy: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.adUnits : undefined, + uniquePbsTid: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.uniquePbsTid : undefined, 'bids': [ { 'bidder': bidderCode || bid.bidderCode, @@ -104,11 +120,13 @@ function mockBidRequest(bid, opts) { }, 'adUnitCode': adUnitCode || ADUNIT_CODE, 'transactionId': bid.transactionId, + adUnitId: bid.adUnitId, 'sizes': [[300, 250], [300, 600]], 'bidId': bid.requestId, 'bidderRequestId': requestId, 'auctionId': opts && opts.auctionId, - 'mediaTypes': mediaType + 'mediaTypes': mediaType, + src: bid?._ctx?.src } ], 'auctionStart': 1505250713622, @@ -160,6 +178,7 @@ describe('auctionmanager.js', function () { indexAuctions = []; indexStub = sinon.stub(auctionManager, 'index'); indexStub.get(() => new AuctionIndex(() => indexAuctions)); + resetAuctionState(); }); afterEach(() => { @@ -515,7 +534,7 @@ describe('auctionmanager.js', function () { s2sConfig: { accountId: '1', enabled: true, - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders: ['appnexus'], timeout: 1000, adapter: 'prebidServer' @@ -763,9 +782,10 @@ describe('auctionmanager.js', function () { }); describe('createAuction', () => { - let adUnits, stubMakeBidRequests, stubCallAdapters + let adUnits, stubMakeBidRequests, stubCallAdapters, bids; beforeEach(() => { + bids = []; stubMakeBidRequests = sinon.stub(adapterManager, 'makeBidRequests').returns([{ bidderCode: BIDDER_CODE, bids: [{ @@ -773,11 +793,12 @@ describe('auctionmanager.js', function () { }] }]); stubCallAdapters = sinon.stub(adapterManager, 'callBids').callsFake((au, reqs, addBid, done) => { + bids.forEach(bid => addBid(bid.adUnitCode, bid)); reqs.forEach(r => done.apply(r)); }); adUnits = [{ code: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE}, ] @@ -787,6 +808,7 @@ describe('auctionmanager.js', function () { afterEach(() => { stubMakeBidRequests.restore(); stubCallAdapters.restore(); + auctionManager.clearAllAuctions(); }); it('passes global and bidder ortb2 to the auction', () => { @@ -814,6 +836,79 @@ describe('auctionmanager.js', function () { }); expect(auction.getNonBids()[0]).to.equal('test'); }); + + describe('stale auctions', () => { + let clock, auction; + beforeEach(() => { + clock = sinon.useFakeTimers(); + auction = auctionManager.createAuction({adUnits}); + indexAuctions.push(auction); + }); + afterEach(() => { + clock.restore(); + config.resetConfig(); + }); + + it('are dropped after their last bid becomes stale (if minBidCacheTTL is set)', () => { + config.setConfig({ + minBidCacheTTL: 0 + }); + bids = [ + { + adUnitCode: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, + ttl: 10 + }, { + adUnitCode: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, + ttl: 100 + } + ]; + auction.callBids(); + return auction.end.then(() => { + clock.tick(50 * 1000); + expect(auctionManager.getBidsReceived().length).to.equal(2); + clock.tick(56 * 1000); + expect(auctionManager.getBidsReceived()).to.eql([]); + }); + }); + + it('are dropped after `minBidCacheTTL` seconds if they had no bid', () => { + auction.callBids(); + config.setConfig({ + minBidCacheTTL: 2 + }); + return auction.end.then(() => { + expect(auctionManager.getNoBids().length).to.eql(1); + clock.tick(10 * 10000); + expect(auctionManager.getNoBids().length).to.eql(0); + }) + }); + + Object.entries({ + 'bids': { + bd: [{ + adUnitCode: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, + ttl: 10 + }], + entries: () => auctionManager.getBidsReceived() + }, + 'no bids': { + bd: [], + entries: () => auctionManager.getNoBids() + } + }).forEach(([t, {bd, entries}]) => { + it(`with ${t} are never dropped if minBidCacheTTL is not set`, () => { + bids = bd; + auction.callBids(); + return auction.end.then(() => { + clock.tick(100 * 1000); + expect(entries().length > 0).to.be.true; + }) + }) + }); + }) }); describe('addBidResponse #1', function () { @@ -839,8 +934,13 @@ describe('auctionmanager.js', function () { beforeEach(function () { ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockAjaxBuilder); adUnits = [{ + mediaTypes: { + banner: { + sizes: [] + } + }, code: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ] @@ -1024,36 +1124,47 @@ describe('auctionmanager.js', function () { assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); }); - it('bid for a regular unit and a video unit', function() { - let renderer = { - url: 'renderer.js', - render: (bid) => bid - }; - Object.assign(adUnits[0], {renderer}); - // make sure that if the renderer is only on the second ad unit, prebid - // still correctly uses it - let bid = mockBid(); - let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; - - bidRequests[0].bids[1] = Object.assign({ - bidId: utils.getUniqueIdentifierStr() - }, bidRequests[0].bids[0]); - Object.assign(bidRequests[0].bids[0], { - adUnitCode: ADUNIT_CODE1, - transactionId: ADUNIT_CODE1, - }); + describe('bid for a regular unit and a video unit', () => { + beforeEach(() => { + const renderer = { + url: 'renderer.js', + render: (bid) => bid + }; + Object.assign(adUnits[0], {renderer}); + // make sure that if the renderer is only on the second ad unit, prebid + // still correctly uses it + let bid = mockBid(); + let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; + + bidRequests[0].bids[1] = Object.assign({ + bidId: utils.getUniqueIdentifierStr() + }, bidRequests[0].bids[0]); + Object.assign(bidRequests[0].bids[0], { + adUnitCode: ADUNIT_CODE1, + adUnitId: ADUNIT_CODE1, + }); - makeRequestsStub.returns(bidRequests); + makeRequestsStub.returns(bidRequests); - // this should correspond with the second bid in the bidReq because of the ad unit code - bid.mediaType = 'video-outstream'; - spec.interpretResponse.returns(bid); + // this should correspond with the second bid in the bidReq because of the ad unit code + bid.mediaType = 'video-outstream'; + spec.interpretResponse.returns(bid); + }); - auction.callBids(); + it('should use renderers on bid response', () => { + auction.callBids(); - const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode == ADUNIT_CODE); - assert.equal(addedBid.renderer.url, 'renderer.js'); - }); + const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode === ADUNIT_CODE); + assert.equal(addedBid.renderer.url, 'renderer.js'); + }); + + it('should resolve .end', () => { + auction.callBids(); + return auction.end.then(() => { + expect(auction.getBidsReceived().length).to.eql(1); + }) + }); + }) it('sets bidResponse.ttlBuffer from adUnit.ttlBuffer', () => { adUnits[0].ttlBuffer = 0; @@ -1063,100 +1174,178 @@ describe('auctionmanager.js', function () { }); describe('when auction timeout is 20', function () { - let eventsEmitSpy; + let eventsEmitSpy, auctionDone; - function setupBids(auctionId) { - bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; - let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId})); + function respondToRequest(requestIndex) { + server.requests[requestIndex].respond(200, {}, 'response body'); + } + + function runAuction() { + let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId: auction.getAuctionId()})); makeRequestsStub.returns(bidRequests); + return new Promise((resolve) => { + auctionDone = resolve; + auction.callBids(); + }) } beforeEach(function () { adUnits = [{ code: ADUNIT_CODE, transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, + {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, ] }]; adUnitCodes = [ADUNIT_CODE]; eventsEmitSpy = sinon.spy(events, 'emit'); + bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; + const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); + registerBidder(spec1); + const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); + registerBidder(spec2); + auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: () => auctionDone(), cbTimeout: 20}); + indexAuctions = [auction]; }); + afterEach(function () { events.emit.restore(); }); - it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); + it('resolves .end on timeout', () => { + let endResolved = false; + auction.end.then(() => { + endResolved = true; + }) + const pm = runAuction().then(() => { + expect(endResolved).to.be.true; + }); + respondToRequest(0); + return pm; + }); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + describe('AUCTION_TIMEOUT event', () => { + let handler; + beforeEach(() => { + handler = sinon.spy(); + events.on(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }) + afterEach(() => { + events.off(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }); + + Object.entries({ + 'is fired on timeout': [true, [0]], + 'is NOT fired otherwise': [false, [0, 1]], + }).forEach(([t, [shouldFire, respond]]) => { + it(t, () => { + const pm = runAuction().then(() => { + if (shouldFire) { + sinon.assert.calledWith(handler, sinon.match({auctionId: auction.getAuctionId()})) + } else { + sinon.assert.notCalled(handler); + } + }); + respond.forEach(respondToRequest); + return pm; + }) + }); + }); + + it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); // Check that additional properties are available - assert.equal(timedOutBids[0].params.placementId, 'id'); + assert.equal(timedOutBids[0].params[0].placementId, 'id'); const auctionEndCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.AUCTION_END).getCalls()[0]; const auctionProps = auctionEndCall.args[1]; assert.equal(auctionProps.adUnits, adUnits); assert.equal(auctionProps.timeout, 20); assert.equal(auctionProps.auctionStatus, AUCTION_COMPLETED) - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - - auction.callBids(); + }); respondToRequest(0); + return pm; }); - it('should NOT emit BID_TIMEOUT when all bidders responded in time', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT when all bidders responded in time', function () { + const pm = runAuction().then(() => { assert.ok(eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).notCalled, 'did not emit event BID_TIMEOUT'); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); respondToRequest(1); + return pm; }); - it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function (done) { - const spec1 = mockBidder(BIDDER_CODE, []); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, []); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); + return pm; }); + + it('should NOT emit BID_TIMEOUT for bidders that replied through S2S', () => { + adapterManager.registerBidAdapter(new PrebidServer(), 'pbs'); + config.setConfig({ + s2sConfig: [{ + accountId: '1', + enabled: true, + defaultVendor: 'appnexuspsp', + bidders: ['mock-s2s-1'], + adapter: 'pbs' + }, { + accountId: '1', + enabled: true, + defaultVendor: 'rubicon', + bidders: ['mock-s2s-2'], + adapter: 'pbs' + }] + }) + adUnits[0].bids.push({bidder: 'mock-s2s-1'}, {bidder: 'mock-s2s-2'}) + const s2sAdUnits = deepClone(adUnits); + bids.unshift( + mockBid({bidderCode: 'mock-s2s-1', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '1'}), + mockBid({bidderCode: 'mock-s2s-2', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '2'}) + ); + Object.assign(s2sAdUnits[0], { + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [ + { + bidder: 'mock-s2s-1', + bid_id: bids[0].requestId + }, + { + bidder: 'mock-s2s-2', + bid_id: bids[1].requestId + } + ] + }) + + const pm = runAuction().then(() => { + const toBids = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0].args[1] + expect(toBids.map(bid => bid.bidder)).to.eql([ + 'mock-s2s-2', + BIDDER_CODE, + BIDDER_CODE1, + ]) + }); + respondToRequest(1); + return pm; + }) }); }); @@ -1176,12 +1365,14 @@ describe('auctionmanager.js', function () { adUnits = [{ code: ADUNIT_CODE, transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ] }, { code: ADUNIT_CODE1, transactionId: ADUNIT_CODE1, + adUnitId: ADUNIT_CODE1, bids: [ {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, ] @@ -1389,6 +1580,7 @@ describe('auctionmanager.js', function () { const adUnits = [{ code: ADUNIT_CODE, transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ], @@ -1521,7 +1713,7 @@ describe('auctionmanager.js', function () { function mockAuction(getBidRequests, start = 1) { return { getBidRequests: getBidRequests, - getAdUnits: () => getBidRequests().flatMap(br => br.bids).map(br => ({ code: br.adUnitCode, transactionId: br.transactionId, mediaTypes: br.mediaTypes })), + getAdUnits: () => getBidRequests().flatMap(br => br.bids).map(br => ({ code: br.adUnitCode, transactionId: br.transactionId, adUnitId: br.adUnitId, mediaTypes: br.mediaTypes })), getAuctionId: () => '1', addBidReceived: () => true, addBidRejected: () => true, @@ -1592,8 +1784,8 @@ describe('auctionmanager.js', function () { let ADUNIT_CODE2 = 'adUnitCode2'; let BIDDER_CODE2 = 'sampleBidder2'; - let bids1 = [mockBid({ bidderCode: BIDDER_CODE1, transactionId: ADUNIT_CODE1 })]; - let bids2 = [mockBid({ bidderCode: BIDDER_CODE2, transactionId: ADUNIT_CODE2 })]; + let bids1 = [mockBid({ bidderCode: BIDDER_CODE1, adUnitId: ADUNIT_CODE1 })]; + let bids2 = [mockBid({ bidderCode: BIDDER_CODE2, adUnitId: ADUNIT_CODE2 })]; bidRequests = [ mockBidRequest(bids[0]), mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), @@ -1651,116 +1843,54 @@ describe('auctionmanager.js', function () { sinon.assert.calledWith(auction.addBidReceived, sinon.match({cpm: 1.23})); }) - describe('when addBidResponse hook returns promises', () => { - let resolvers, callbacks, bids; + describe('when responsesReady defers', () => { + let resolve, reject, promise, callbacks, bids; - function hook(next, ...args) { - next.bail(new Promise((resolve, reject) => { - resolvers.resolve.push(resolve); - resolvers.reject.push(reject); - }).finally(() => next(...args))); + function hook(next, ready) { + next(ready.then(() => promise)); } - function invokeCallbacks() { - bids.forEach((bid) => callbacks.addBidResponse(ADUNIT_CODE, bid)); - bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); - } + before(() => { + responsesReady.before(hook); + }); - function delay(ms = 0) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }); - } + after(() => { + responsesReady.getHooks({hook}).remove(); + }); beforeEach(() => { + // eslint-disable-next-line promise/param-names + promise = new Promise((rs, rj) => { + resolve = rs; + reject = rj; + }); bids = [ mockBid({bidderCode: BIDDER_CODE1}), mockBid({bidderCode: BIDDER_CODE}) ] bidRequests = bids.map((b) => mockBidRequest(b)); - resolvers = {resolve: [], reject: []}; - addBidResponse.before(hook); callbacks = auctionCallbacks(doneSpy, auction); Object.assign(auction, { addNoBid: sinon.spy() }); }); - afterEach(() => { - addBidResponse.getHooks({hook: hook}).remove(); - }); - - it('should wait for bids without a request bids before calling auctionDone', () => { - callbacks.addBidResponse(ADUNIT_CODE, Object.assign(mockBid(), {requestId: null})); - invokeCallbacks(); - resolvers.resolve.slice(1, 3).forEach((fn) => fn()); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - resolvers.resolve[0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - Object.entries({ - 'all succeed': ['resolve', 'resolve'], - 'some fail': ['resolve', 'reject'], - 'all fail': ['reject', 'reject'] - }).forEach(([test, results]) => { - describe(`(and ${test})`, () => { - it('should wait for them to complete before calling auctionDone', () => { - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[0]][0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[1]][1](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); + 'resolve': () => resolve(), + 'reject': () => reject(), + }).forEach(([t, resolver]) => { + it(`should wait for responsesReady to ${t} before calling auctionDone`, (done) => { + bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); + setTimeout(() => { + sinon.assert.notCalled(doneSpy); + resolver(); + setTimeout(() => { + sinon.assert.called(doneSpy); + done(); + }) + }) }); }); - - Object.entries({ - bidder: (timeout) => { - bidRequests.forEach((r) => r.timeout = timeout); - auction.getTimeout = () => timeout + 10000 - }, - auction: (timeout) => { - auction.getTimeout = () => timeout; - bidRequests.forEach((r) => r.timeout = timeout + 10000) - } - }).forEach(([test, setTimeout]) => { - it(`should respect ${test} timeout if they never complete`, () => { - const start = Date.now() - 2900; - auction.getAuctionStart = () => start; - setTimeout(3000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - return delay(100); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - - it(`should not wait if ${test} has already timed out`, () => { - const start = Date.now() - 2000; - auction.getAuctionStart = () => start; - setTimeout(1000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - }) }); describe('when bids are rejected', () => { diff --git a/test/spec/creative/crossDomainCreative_spec.js b/test/spec/creative/crossDomainCreative_spec.js new file mode 100644 index 00000000000..f4c98aa7b50 --- /dev/null +++ b/test/spec/creative/crossDomainCreative_spec.js @@ -0,0 +1,194 @@ +import {renderer} from '../../../creative/crossDomain.js'; +import { + ERROR_EXCEPTION, + EVENT_AD_RENDER_FAILED, EVENT_AD_RENDER_SUCCEEDED, + MESSAGE_EVENT, + MESSAGE_REQUEST, + MESSAGE_RESPONSE +} from '../../../creative/constants.js'; + +describe('cross-domain creative', () => { + const ORIGIN = 'https://example.com'; + let win, renderAd, messages, mkIframe; + + beforeEach(() => { + messages = []; + mkIframe = sinon.stub(); + win = { + document: { + body: { + appendChild: sinon.stub(), + }, + createElement: sinon.stub().callsFake(tagname => { + switch (tagname.toLowerCase()) { + case 'a': + return document.createElement('a') + case 'iframe': { + return mkIframe(); + } + } + }) + }, + parent: { + postMessage: sinon.stub().callsFake((payload, targetOrigin, transfer) => { + messages.push({payload: JSON.parse(payload), targetOrigin, transfer}); + }) + } + }; + renderAd = renderer(win); + }) + + it('derives postMessage target origin from pubUrl ', () => { + renderAd({pubUrl: 'https://domain.com:123/path'}); + expect(messages[0].targetOrigin).to.eql('https://domain.com:123') + }); + + it('generates request message with adId and clickUrl', () => { + renderAd({adId: '123', clickUrl: 'https://click-url.com', pubUrl: ORIGIN}); + expect(messages[0].payload).to.eql({ + message: MESSAGE_REQUEST, + adId: '123', + options: { + clickUrl: 'https://click-url.com' + } + }) + }); + + it('runs scripts inserted through iframe srcdoc', (done) => { + const iframe = document.createElement('iframe'); + iframe.setAttribute('srcdoc', ''); + iframe.onload = function () { + expect(iframe.contentWindow.ran).to.be.true; + done(); + } + document.body.appendChild(iframe); + }) + + describe('listens and', () => { + function reply(msg, index = 0) { + messages[index].transfer[0].postMessage(JSON.stringify(msg)); + } + + it('ignores messages that are not a prebid response message', () => { + renderAd({adId: '123', pubUrl: ORIGIN}); + reply({adId: '123', ad: 'markup'}); + sinon.assert.notCalled(mkIframe); + }) + + it('signals AD_RENDER_FAILED on exceptions', (done) => { + mkIframe.callsFake(() => { throw new Error('error message') }); + renderAd({adId: '123', pubUrl: ORIGIN}); + reply({message: MESSAGE_RESPONSE, adId: '123', ad: 'markup'}); + setTimeout(() => { + expect(messages[1].payload).to.eql({ + message: MESSAGE_EVENT, + adId: '123', + event: EVENT_AD_RENDER_FAILED, + info: { + reason: ERROR_EXCEPTION, + message: 'error message' + } + }) + done(); + }, 100) + }); + + describe('renderer', () => { + beforeEach(() => { + win.document.createElement.callsFake(document.createElement.bind(document)); + win.document.body.appendChild.callsFake(document.body.appendChild.bind(document.body)); + }); + + it('sets up and runs renderer', (done) => { + window._render = sinon.stub(); + const data = { + message: MESSAGE_RESPONSE, + adId: '123', + renderer: 'window.render = window.parent._render' + } + renderAd({adId: '123', pubUrl: ORIGIN}); + reply(data); + setTimeout(() => { + try { + sinon.assert.calledWith(window._render, data, sinon.match.any, win); + done() + } finally { + delete window._render; + } + }, 100) + }); + + Object.entries({ + 'throws (w/error)': ['window.render = function() { throw new Error("msg") }'], + 'throws (w/reason)': ['window.render = function() { throw {reason: "other", message: "msg"}}', 'other'], + 'is missing': [null, ERROR_EXCEPTION, null], + 'rejects (w/error)': ['window.render = function() { return Promise.reject(new Error("msg")) }'], + 'rejects (w/reason)': ['window.render = function() { return Promise.reject({reason: "other", message: "msg"}) }', 'other'], + }).forEach(([t, [renderer, reason = ERROR_EXCEPTION, message = 'msg']]) => { + it(`signals AD_RENDER_FAILED on renderer that ${t}`, (done) => { + renderAd({adId: '123', pubUrl: ORIGIN}); + reply({ + message: MESSAGE_RESPONSE, + adId: '123', + renderer + }); + setTimeout(() => { + sinon.assert.match(messages[1].payload, { + adId: '123', + message: MESSAGE_EVENT, + event: EVENT_AD_RENDER_FAILED, + info: { + reason, + message: sinon.match(val => message == null || message === val) + } + }); + done(); + }, 100) + }) + }); + + it('signals AD_RENDER_SUCCEEDED when renderer resolves', (done) => { + renderAd({adId: '123', pubUrl: ORIGIN}); + reply({ + message: MESSAGE_RESPONSE, + adId: '123', + renderer: 'window.render = function() { return new Promise((resolve) => { window.parent._resolve = resolve })}' + }); + setTimeout(() => { + expect(messages[1]).to.not.exist; + window._resolve(); + setTimeout(() => { + sinon.assert.match(messages[1].payload, { + adId: '123', + message: MESSAGE_EVENT, + event: EVENT_AD_RENDER_SUCCEEDED + }) + delete window._resolve; + done(); + }, 100) + }, 100) + }) + + it('is provided a sendMessage that accepts replies', (done) => { + renderAd({adId: '123', pubUrl: ORIGIN}); + window._reply = sinon.stub(); + reply({ + adId: '123', + message: MESSAGE_RESPONSE, + renderer: 'window.render = function(_, {sendMessage}) { sendMessage("test", "data", function(reply) { window.parent._reply(reply) }) }' + }); + setTimeout(() => { + reply('response', 1); + setTimeout(() => { + try { + sinon.assert.calledWith(window._reply, sinon.match({data: JSON.stringify('response')})); + done(); + } finally { + delete window._reply; + } + }, 100) + }, 100) + }); + }); + }); +}); diff --git a/test/spec/creative/displayRenderer_spec.js b/test/spec/creative/displayRenderer_spec.js new file mode 100644 index 00000000000..6be6e90813a --- /dev/null +++ b/test/spec/creative/displayRenderer_spec.js @@ -0,0 +1,55 @@ +import {render} from 'creative/renderers/display/renderer.js'; +import {ERROR_NO_AD} from '../../../creative/renderers/display/constants.js'; + +describe('Creative renderer - display', () => { + let doc, mkFrame, sendMessage; + beforeEach(() => { + mkFrame = sinon.stub().callsFake((doc, attrs) => Object.assign({doc}, attrs)); + sendMessage = sinon.stub(); + doc = { + body: { + appendChild: sinon.stub() + } + }; + }); + + function runRenderer(data) { + return render(data, {sendMessage, mkFrame}, {document: doc}); + } + + it('throws when both ad and adUrl are missing', () => { + expect(() => { + try { + runRenderer({}) + } catch (e) { + expect(e.reason).to.eql(ERROR_NO_AD); + throw e; + } + }).to.throw(); + }) + + Object.entries({ + ad: 'srcdoc', + adUrl: 'src' + }).forEach(([adProp, frameProp]) => { + describe(`when ad has ${adProp}`, () => { + let data; + beforeEach(() => { + data = { + [adProp]: 'ad', + width: 123, + height: 321 + } + }) + it(`drops iframe with ${frameProp} = ${adProp}`, () => { + runRenderer(data); + sinon.assert.calledWith(doc.body.appendChild, { + doc, + [frameProp]: 'ad', + width: data.width, + height: data.height + }) + }) + }) + }) +}) diff --git a/test/spec/creative/nativeRenderer_spec.js b/test/spec/creative/nativeRenderer_spec.js new file mode 100644 index 00000000000..66e81a517c7 --- /dev/null +++ b/test/spec/creative/nativeRenderer_spec.js @@ -0,0 +1,298 @@ +import {getAdMarkup, getReplacements, getReplacer} from '../../../creative/renderers/native/renderer.js'; +import {ACTION_CLICK, ACTION_IMP, ACTION_RESIZE, MESSAGE_NATIVE} from '../../../creative/renderers/native/constants.js'; + +describe('Native creative renderer', () => { + let win; + beforeEach(() => { + win = {}; + }); + + describe('getAdMarkup', () => { + let loadScript; + beforeEach(() => { + loadScript = sinon.stub(); + }); + it('uses rendererUrl if present', () => { + win.document = {} + const data = { + assets: ['1', '2'], + ortb: 'ortb', + rendererUrl: 'renderer' + }; + const renderAd = sinon.stub().returns('markup'); + loadScript.returns(Promise.resolve().then(() => { + win.renderAd = renderAd; + })); + return getAdMarkup('123', data, null, win, loadScript).then((markup) => { + expect(markup).to.eql('markup'); + sinon.assert.calledWith(loadScript, data.rendererUrl, sinon.match(arg => arg === win.document)); + sinon.assert.calledWith(renderAd, sinon.match(arg => { + expect(arg).to.have.members(data.assets); + expect(arg.ortb).to.eql(data.ortb); + return true; + })); + }); + }); + describe('otherwise, calls replacer', () => { + let replacer; + beforeEach(() => { + replacer = sinon.stub().returns('markup'); + }); + it('with adTemplate, if present', () => { + return getAdMarkup('123', {adTemplate: 'tpl'}, replacer, win).then((result) => { + expect(result).to.eql('markup'); + sinon.assert.calledWith(replacer, 'tpl'); + }); + }); + it('with document body otherwise', () => { + win.document = {body: {innerHTML: 'body'}}; + return getAdMarkup('123', {}, replacer, win).then((result) => { + expect(result).to.eql('markup'); + sinon.assert.calledWith(replacer, 'body'); + }) + }) + }) + }); + + describe('getReplacer', () => { + function expectReplacements(replacer, replacements) { + Object.entries(replacements).forEach(([placeholder, repl]) => { + expect(replacer(`.${placeholder}.${placeholder}.`)).to.eql(`.${repl}.${repl}.`); + }) + } + it('uses empty strings for missing legacy assets', () => { + const repl = getReplacer('123', { + nativeKeys: { + 'k': 'hb_native_k' + } + }); + expectReplacements(repl, { + '##hb_native_k##': '', + 'hb_native_k:123': '' + }) + }); + + it('uses empty string for missing ORTB assets', () => { + const repl = getReplacer('', { + ortb: { + assets: [{ + id: 1, + link: {url: 'l1'}, + data: {value: 'v1'} + }] + } + }); + expectReplacements(repl, { + '##hb_native_asset_id_1##': 'v1', + '##hb_native_asset_id_2##': '', + '##hb_native_asset_link_id_1##': 'l1', + '##hb_native_asset_link_id_2##': '' + }); + }); + + it('replaces placeholders for for legacy assets', () => { + const repl = getReplacer('123', { + assets: [ + {key: 'k1', value: 'v1'}, {key: 'k2', value: 'v2'} + ], + nativeKeys: { + k1: 'hb_native_k1', + k2: 'hb_native_k2' + } + }); + expectReplacements(repl, { + '##hb_native_k1##': 'v1', + 'hb_native_k1:123': 'v1', + '##hb_native_k2##': 'v2', + 'hb_native_k2:123': 'v2' + }) + }); + + describe('ORTB response top-level (non-asset) fields', () => { + const ortb = { + link: { + url: 'link.url' + }, + privacy: 'privacy.url' + }; + const expected = { + '##hb_native_linkurl##': 'link.url', + '##hb_native_privacy##': 'privacy.url' + }; + it('replaces placeholders', () => { + const repl = getReplacer('123', { + ortb + }); + expectReplacements(repl, expected); + }); + it('gives them precedence over legacy counterparts', () => { + const repl = getReplacer('123', { + ortb, + assets: [ + {key: 'clickUrl', value: 'overridden'}, + {key: 'privacyLink', value: 'overridden'} + ], + nativeKeys: { + clickUrl: 'hb_native_linkurl', + privacyLink: 'hb_native_privacy' + } + }); + expectReplacements(repl, expected); + }); + it('uses empty string for missing assets', () => { + const repl = getReplacer('123', { + ortb: {} + }); + expectReplacements(repl, { + '##hb_native_linkurl##': '', + '##hb_native_privacy##': '', + }) + }); + }); + + Object.entries({ + title: {text: 'val'}, + data: {value: 'val'}, + img: {url: 'val'}, + video: {vasttag: 'val'} + }).forEach(([type, contents]) => { + describe(`for ortb ${type} asset`, () => { + let ortb; + beforeEach(() => { + ortb = { + assets: [ + { + id: 123, + [type]: contents + } + ] + }; + }); + it('replaces placeholder', () => { + const repl = getReplacer('', {ortb}); + expectReplacements(repl, { + '##hb_native_asset_id_123##': 'val' + }) + }); + it('replaces link placeholders', () => { + ortb.assets[0].link = {url: 'link'}; + const repl = getReplacer('', {ortb}); + expectReplacements(repl, { + '##hb_native_asset_link_id_123##': 'link' + }) + }); + }); + }); + }); + + describe('render', () => { + let getMarkup, sendMessage, adId, nativeData, exc; + beforeEach(() => { + adId = '123'; + nativeData = {} + getMarkup = sinon.stub(); + sendMessage = sinon.stub() + exc = sinon.stub(); + win.document = { + querySelectorAll() { return [] }, + body: {} + } + }); + + function runRender() { + return render({adId, native: nativeData}, {sendMessage, exc}, win, getMarkup) + } + + it('replaces placeholders in head, if present', () => { + getMarkup.returns(Promise.resolve('')) + win.document.head = {innerHTML: '##hb_native_asset_id_1##'}; + nativeData.ortb = { + assets: [ + {id: 1, data: {value: 'repl'}} + ] + }; + return runRender().then(() => { + expect(win.document.head.innerHTML).to.eql('repl'); + }) + }); + + it('drops markup on body, and fires imp trackers', () => { + getMarkup.returns(Promise.resolve('markup')); + return runRender().then(() => { + expect(win.document.body.innerHTML).to.eql('markup'); + sinon.assert.calledWith(sendMessage, MESSAGE_NATIVE, {action: ACTION_IMP}); + }) + }); + + it('runs postRenderAd if defined', () => { + win.postRenderAd = sinon.stub(); + getMarkup.returns(Promise.resolve('markup')); + return runRender().then(() => { + sinon.assert.calledWith(win.postRenderAd, sinon.match({ + adId, + ...nativeData + })) + }) + }) + + it('rejects on error', (done) => { + const err = new Error('failure'); + getMarkup.returns(Promise.reject(err)); + runRender().catch((e) => { + expect(e).to.eql(err); + done(); + }) + }); + + describe('requests resize', () => { + beforeEach(() => { + getMarkup.returns(Promise.resolve('markup')); + win.document.body.offsetHeight = 123; + win.document.body.offsetWidth = 321; + }); + + it('immediately, if document is loaded', () => { + win.document.readyState = 'complete'; + return runRender().then(() => { + sinon.assert.calledWith(sendMessage, MESSAGE_NATIVE, {action: ACTION_RESIZE, height: 123, width: 321}) + }) + }); + + it('on document load otherwise', () => { + return runRender().then(() => { + sinon.assert.neverCalledWith(sendMessage, MESSAGE_NATIVE, sinon.match({action: ACTION_RESIZE})); + win.onload(); + sinon.assert.calledWith(sendMessage, MESSAGE_NATIVE, {action: ACTION_RESIZE, height: 123, width: 321}); + }) + }) + }) + + describe('click trackers', () => { + let iframe; + beforeEach(() => { + iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + win.document = iframe.contentDocument; + }) + afterEach(() => { + document.body.removeChild(iframe); + }) + + it('are fired on click', () => { + getMarkup.returns(Promise.resolve('
')); + return runRender().then(() => { + win.document.querySelector('#target').click(); + sinon.assert.calledWith(sendMessage, MESSAGE_NATIVE, sinon.match({action: ACTION_CLICK})); + }) + }); + + it('pass assetId if provided', () => { + getMarkup.returns(Promise.resolve('
')); + return runRender().then(() => { + win.document.querySelector('#target').click(); + sinon.assert.calledWith(sendMessage, MESSAGE_NATIVE, {action: ACTION_CLICK, assetId: '123'}) + }); + }); + }); + }); +}); diff --git a/test/spec/e2e/banner/basic_banner_ad.spec.js b/test/spec/e2e/banner/basic_banner_ad.spec.js index e8103581d9d..511b1002d80 100644 --- a/test/spec/e2e/banner/basic_banner_ad.spec.js +++ b/test/spec/e2e/banner/basic_banner_ad.spec.js @@ -19,8 +19,8 @@ setupTest({ waitFor: CREATIVE_IFRAME_CSS_SELECTOR, expectGAMCreative: true }, 'Prebid.js Banner Ad Unit Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.pbjs.getAdserverTargeting('div-gpt-ad-1460505748561-1'); }); const targetingKeys = result['div-gpt-ad-1460505748561-1']; diff --git a/test/spec/e2e/instream/basic_instream_video_ad.spec.js b/test/spec/e2e/instream/basic_instream_video_ad.spec.js index 02d218f9175..ca5296f050c 100644 --- a/test/spec/e2e/instream/basic_instream_video_ad.spec.js +++ b/test/spec/e2e/instream/basic_instream_video_ad.spec.js @@ -18,8 +18,8 @@ setupTest({ url: TEST_PAGE_URL, waitFor: ALERT_BOX_CSS_SELECTOR, }, 'Prebid.js Instream Video Ad Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.top.pbjs.getAdserverTargeting('video1'); }); diff --git a/test/spec/e2e/longform/basic_w_bidderSettings.spec.js b/test/spec/e2e/longform/basic_w_bidderSettings.spec.js index e8bdbcf3b4f..1b884aeca1b 100644 --- a/test/spec/e2e/longform/basic_w_bidderSettings.spec.js +++ b/test/spec/e2e/longform/basic_w_bidderSettings.spec.js @@ -10,25 +10,25 @@ const uuidRegex = /(\d|\w){8}-((\d|\w){4}-){3}(\d|\w){12}/; describe('longform ads not using requireExactDuration field', function() { this.retries(3); - it('process the bids successfully', function() { - browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_bidderSettings.html?pbjs_debug=true'); - browser.pause(7000); + it('process the bids successfully', async function() { + await browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_bidderSettings.html?pbjs_debug=true'); + await browser.pause(7000); const loadPrebidBtnXpath = '//*[@id="loadPrebidRequestBtn"]'; - waitForElement(loadPrebidBtnXpath, 3000); - const prebidBtn = $(loadPrebidBtnXpath); - prebidBtn.click(); - browser.pause(5000); + await waitForElement(loadPrebidBtnXpath, 3000); + const prebidBtn = await $(loadPrebidBtnXpath); + await prebidBtn.click(); + await browser.pause(5000); const listOfCpmsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[2]'; const listOfCategoriesXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[3]'; const listOfDurationsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[4]'; - waitForElement(listOfCpmsXpath, 3000); + await waitForElement(listOfCpmsXpath, 3000); - let listOfCpms = $$(listOfCpmsXpath); - let listOfCats = $$(listOfCategoriesXpath); - let listOfDuras = $$(listOfDurationsXpath); + let listOfCpms = await $$(listOfCpmsXpath); + let listOfCats = await $$(listOfCategoriesXpath); + let listOfDuras = await $$(listOfDurationsXpath); expect(listOfCpms.length).to.equal(listOfCats.length).and.to.equal(listOfDuras.length); for (let i = 0; i < listOfCpms.length; i++) { @@ -41,14 +41,14 @@ describe('longform ads not using requireExactDuration field', function() { } }); - it('formats the targeting keys properly', function () { + it('formats the targeting keys properly', async function () { const listOfKeyElementsXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[1]'; const listOfKeyValuesXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[2]'; - waitForElement(listOfKeyElementsXpath); - waitForElement(listOfKeyValuesXpath); + await waitForElement(listOfKeyElementsXpath); + await waitForElement(listOfKeyValuesXpath); - let listOfKeyElements = $$(listOfKeyElementsXpath); - let listOfKeyValues = $$(listOfKeyValuesXpath); + let listOfKeyElements = await $$(listOfKeyElementsXpath); + let listOfKeyValues = await $$(listOfKeyValuesXpath); let firstKey = listOfKeyElements[0].getText(); expect(firstKey).to.equal('hb_pb_cat_dur'); diff --git a/test/spec/e2e/longform/basic_w_custom_adserver_translation.spec.js b/test/spec/e2e/longform/basic_w_custom_adserver_translation.spec.js index e4ea87bab1a..e66c9eb0cd5 100644 --- a/test/spec/e2e/longform/basic_w_custom_adserver_translation.spec.js +++ b/test/spec/e2e/longform/basic_w_custom_adserver_translation.spec.js @@ -10,25 +10,25 @@ const uuidRegex = /(\d|\w){8}-((\d|\w){4}-){3}(\d|\w){12}/; describe('longform ads using custom adserver translation file', function() { this.retries(3); - it('process the bids successfully', function() { - browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_custom_adserver_translation.html?pbjs_debug=true'); - browser.pause(7000); + it('process the bids successfully', async function() { + await browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_custom_adserver_translation.html?pbjs_debug=true'); + await browser.pause(7000); const loadPrebidBtnXpath = '//*[@id="loadPrebidRequestBtn"]'; - waitForElement(loadPrebidBtnXpath, 3000); - const prebidBtn = $(loadPrebidBtnXpath); - prebidBtn.click(); - browser.pause(5000); + await waitForElement(loadPrebidBtnXpath, 3000); + const prebidBtn = await $(loadPrebidBtnXpath); + await prebidBtn.click(); + await browser.pause(5000); const listOfCpmsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[2]'; const listOfCategoriesXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[3]'; const listOfDurationsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[4]'; - waitForElement(listOfCpmsXpath); + await waitForElement(listOfCpmsXpath); - let listOfCpms = $$(listOfCpmsXpath); - let listOfCats = $$(listOfCategoriesXpath); - let listOfDuras = $$(listOfDurationsXpath); + let listOfCpms = await $$(listOfCpmsXpath); + let listOfCats = await $$(listOfCategoriesXpath); + let listOfDuras = await $$(listOfDurationsXpath); expect(listOfCpms.length).to.equal(listOfCats.length).and.to.equal(listOfDuras.length); for (let i = 0; i < listOfCpms.length; i++) { @@ -41,14 +41,14 @@ describe('longform ads using custom adserver translation file', function() { } }); - it('formats the targeting keys properly', function () { + it('formats the targeting keys properly', async function () { const listOfKeyElementsXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[1]'; const listOfKeyValuesXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[2]'; - waitForElement(listOfKeyElementsXpath); - waitForElement(listOfKeyValuesXpath); + await waitForElement(listOfKeyElementsXpath); + await waitForElement(listOfKeyValuesXpath); - let listOfKeyElements = $$(listOfKeyElementsXpath); - let listOfKeyValues = $$(listOfKeyValuesXpath); + let listOfKeyElements = await $$(listOfKeyElementsXpath); + let listOfKeyValues = await $$(listOfKeyValuesXpath); let firstKey = listOfKeyElements[0].getText(); expect(firstKey).to.equal('hb_pb_cat_dur'); diff --git a/test/spec/e2e/longform/basic_w_priceGran.spec.js b/test/spec/e2e/longform/basic_w_priceGran.spec.js index b4a5272a69c..df375fb1d39 100644 --- a/test/spec/e2e/longform/basic_w_priceGran.spec.js +++ b/test/spec/e2e/longform/basic_w_priceGran.spec.js @@ -10,25 +10,25 @@ const uuidRegex = /(\d|\w){8}-((\d|\w){4}-){3}(\d|\w){12}/; describe('longform ads not using requireExactDuration field', function() { this.retries(3); - it('process the bids successfully', function() { - browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_priceGran.html?pbjs_debug=true'); - browser.pause(7000); + it('process the bids successfully', async function() { + await browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_priceGran.html?pbjs_debug=true'); + await browser.pause(7000); const loadPrebidBtnXpath = '//*[@id="loadPrebidRequestBtn"]'; - waitForElement(loadPrebidBtnXpath); - const prebidBtn = $(loadPrebidBtnXpath); - prebidBtn.click(); - browser.pause(5000); + await waitForElement(loadPrebidBtnXpath); + const prebidBtn = await $(loadPrebidBtnXpath); + await prebidBtn.click(); + await browser.pause(5000); const listOfCpmsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[2]'; const listOfCategoriesXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[3]'; const listOfDurationsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[4]'; - waitForElement(listOfCpmsXpath); + await waitForElement(listOfCpmsXpath); - let listOfCpms = $$(listOfCpmsXpath); - let listOfCats = $$(listOfCategoriesXpath); - let listOfDuras = $$(listOfDurationsXpath); + let listOfCpms = await $$(listOfCpmsXpath); + let listOfCats = await $$(listOfCategoriesXpath); + let listOfDuras = await $$(listOfDurationsXpath); expect(listOfCpms.length).to.equal(listOfCats.length).and.to.equal(listOfDuras.length); for (let i = 0; i < listOfCpms.length; i++) { @@ -41,14 +41,14 @@ describe('longform ads not using requireExactDuration field', function() { } }); - it('formats the targeting keys properly', function () { + it('formats the targeting keys properly', async function () { const listOfKeyElementsXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[1]'; const listOfKeyValuesXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[2]'; - waitForElement(listOfKeyElementsXpath); - waitForElement(listOfKeyValuesXpath); + await waitForElement(listOfKeyElementsXpath); + await waitForElement(listOfKeyValuesXpath); - let listOfKeyElements = $$(listOfKeyElementsXpath); - let listOfKeyValues = $$(listOfKeyValuesXpath); + let listOfKeyElements = await $$(listOfKeyElementsXpath); + let listOfKeyValues = await $$(listOfKeyValuesXpath); let firstKey = listOfKeyElements[0].getText(); expect(firstKey).to.equal('hb_pb_cat_dur'); diff --git a/test/spec/e2e/longform/basic_w_requireExactDuration.spec.js b/test/spec/e2e/longform/basic_w_requireExactDuration.spec.js index 6f9acf33061..f36c5815750 100644 --- a/test/spec/e2e/longform/basic_w_requireExactDuration.spec.js +++ b/test/spec/e2e/longform/basic_w_requireExactDuration.spec.js @@ -10,25 +10,25 @@ const uuidRegex = /(\d|\w){8}-((\d|\w){4}-){3}(\d|\w){12}/; describe('longform ads using requireExactDuration field', function() { this.retries(3); - it('process the bids successfully', function() { - browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_requireExactDuration.html?pbjs_debug=true'); - browser.pause(7000); + it('process the bids successfully', async function() { + await browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_w_requireExactDuration.html?pbjs_debug=true'); + await browser.pause(7000); const loadPrebidBtnXpath = '//*[@id="loadPrebidRequestBtn"]'; - waitForElement(loadPrebidBtnXpath); - const prebidBtn = $(loadPrebidBtnXpath); - prebidBtn.click(); - browser.pause(5000); + await waitForElement(loadPrebidBtnXpath); + const prebidBtn = await $(loadPrebidBtnXpath); + await prebidBtn.click(); + await browser.pause(5000); const listOfCpmsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[2]'; const listOfCategoriesXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[3]'; const listOfDurationsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[4]'; - waitForElement(listOfCpmsXpath); + await waitForElement(listOfCpmsXpath); - let listOfCpms = $$(listOfCpmsXpath); - let listOfCats = $$(listOfCategoriesXpath); - let listOfDuras = $$(listOfDurationsXpath); + let listOfCpms = await $$(listOfCpmsXpath); + let listOfCats = await $$(listOfCategoriesXpath); + let listOfDuras = await $$(listOfDurationsXpath); expect(listOfCpms.length).to.equal(listOfCats.length).and.to.equal(listOfDuras.length); for (let i = 0; i < listOfCpms.length; i++) { @@ -41,14 +41,14 @@ describe('longform ads using requireExactDuration field', function() { } }); - it('formats the targeting keys properly', function () { + it('formats the targeting keys properly', async function () { const listOfKeyElementsXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[1]'; const listOfKeyValuesXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[2]'; - waitForElement(listOfKeyElementsXpath); - waitForElement(listOfKeyValuesXpath); + await waitForElement(listOfKeyElementsXpath); + await waitForElement(listOfKeyValuesXpath); - let listOfKeyElements = $$(listOfKeyElementsXpath); - let listOfKeyValues = $$(listOfKeyValuesXpath); + let listOfKeyElements = await $$(listOfKeyElementsXpath); + let listOfKeyValues = await $$(listOfKeyValuesXpath); let firstKey = listOfKeyElements[0].getText(); expect(firstKey).to.equal('hb_pb_cat_dur'); diff --git a/test/spec/e2e/longform/basic_wo_brandCategoryExclusion.spec.js b/test/spec/e2e/longform/basic_wo_brandCategoryExclusion.spec.js index 1775bfafa77..2a10e46fc6d 100644 --- a/test/spec/e2e/longform/basic_wo_brandCategoryExclusion.spec.js +++ b/test/spec/e2e/longform/basic_wo_brandCategoryExclusion.spec.js @@ -9,23 +9,23 @@ const uuidRegex = /(\d|\w){8}-((\d|\w){4}-){3}(\d|\w){12}/; describe('longform ads without using brandCategoryExclusion', function() { this.retries(3); - it('process the bids successfully', function() { - browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_wo_brandCategoryExclusion.html?pbjs_debug=true'); - browser.pause(7000); + it('process the bids successfully', async function() { + await browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_wo_brandCategoryExclusion.html?pbjs_debug=true'); + await browser.pause(7000); const loadPrebidBtnXpath = '//*[@id="loadPrebidRequestBtn"]'; - waitForElement(loadPrebidBtnXpath); - const prebidBtn = $(loadPrebidBtnXpath); - prebidBtn.click(); - browser.pause(5000); + await waitForElement(loadPrebidBtnXpath); + const prebidBtn = await $(loadPrebidBtnXpath); + await prebidBtn.click(); + await browser.pause(5000); const listOfCpmsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[2]'; const listOfDurationsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[4]'; - waitForElement(listOfCpmsXpath); + await waitForElement(listOfCpmsXpath); - let listOfCpms = $$(listOfCpmsXpath); - let listOfDuras = $$(listOfDurationsXpath); + let listOfCpms = await $$(listOfCpmsXpath); + let listOfDuras = await $$(listOfDurationsXpath); expect(listOfCpms.length).to.equal(listOfDuras.length); for (let i = 0; i < listOfCpms.length; i++) { @@ -36,14 +36,14 @@ describe('longform ads without using brandCategoryExclusion', function() { } }); - it('formats the targeting keys properly', function () { + it('formats the targeting keys properly', async function () { const listOfKeyElementsXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[1]'; const listOfKeyValuesXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[2]'; - waitForElement(listOfKeyElementsXpath); - waitForElement(listOfKeyValuesXpath); + await waitForElement(listOfKeyElementsXpath); + await waitForElement(listOfKeyValuesXpath); - let listOfKeyElements = $$(listOfKeyElementsXpath); - let listOfKeyValues = $$(listOfKeyValuesXpath); + let listOfKeyElements = await $$(listOfKeyElementsXpath); + let listOfKeyValues = await $$(listOfKeyValuesXpath); let firstKey = listOfKeyElements[0].getText(); expect(firstKey).to.equal('hb_pb_cat_dur'); diff --git a/test/spec/e2e/longform/basic_wo_requireExactDuration.spec.js b/test/spec/e2e/longform/basic_wo_requireExactDuration.spec.js index 9e92c15e5f5..a2974edca11 100644 --- a/test/spec/e2e/longform/basic_wo_requireExactDuration.spec.js +++ b/test/spec/e2e/longform/basic_wo_requireExactDuration.spec.js @@ -10,25 +10,25 @@ const uuidRegex = /(\d|\w){8}-((\d|\w){4}-){3}(\d|\w){12}/; describe('longform ads not using requireExactDuration field', function() { this.retries(3); - it('process the bids successfully', function() { + it('process the bids successfully', async function() { browser.url(protocol + '://' + host + ':9999/integrationExamples/longform/basic_wo_requireExactDuration.html?pbjs_debug=true'); browser.pause(7000); const loadPrebidBtnXpath = '//*[@id="loadPrebidRequestBtn"]'; - waitForElement(loadPrebidBtnXpath); - const prebidBtn = $(loadPrebidBtnXpath); - prebidBtn.click(); - browser.pause(5000); + await waitForElement(loadPrebidBtnXpath); + const prebidBtn = await $(loadPrebidBtnXpath); + await prebidBtn.click(); + await browser.pause(5000); const listOfCpmsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[2]'; const listOfCategoriesXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[3]'; const listOfDurationsXpath = '/html/body/div[1]/div/div/div/div[1]/div[2]/div/table/tbody/tr/td[4]'; - waitForElement(listOfCpmsXpath); + await waitForElement(listOfCpmsXpath); - let listOfCpms = $$(listOfCpmsXpath); - let listOfCats = $$(listOfCategoriesXpath); - let listOfDuras = $$(listOfDurationsXpath); + let listOfCpms = await $$(listOfCpmsXpath); + let listOfCats = await $$(listOfCategoriesXpath); + let listOfDuras = await $$(listOfDurationsXpath); expect(listOfCpms.length).to.equal(listOfCats.length).and.to.equal(listOfDuras.length); for (let i = 0; i < listOfCpms.length; i++) { @@ -41,14 +41,14 @@ describe('longform ads not using requireExactDuration field', function() { } }); - it('formats the targeting keys properly', function () { + it('formats the targeting keys properly', async function () { const listOfKeyElementsXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[1]'; const listOfKeyValuesXpath = '/html/body/div[1]/div/div/div/div[2]/div[2]/div/table/tbody/tr/td[2]'; - waitForElement(listOfKeyElementsXpath); - waitForElement(listOfKeyValuesXpath); + await waitForElement(listOfKeyElementsXpath); + await waitForElement(listOfKeyValuesXpath); - let listOfKeyElements = $$(listOfKeyElementsXpath); - let listOfKeyValues = $$(listOfKeyValuesXpath); + let listOfKeyElements = await $$(listOfKeyElementsXpath); + let listOfKeyValues = await $$(listOfKeyValuesXpath); let firstKey = listOfKeyElements[0].getText(); expect(firstKey).to.equal('hb_pb_cat_dur'); diff --git a/test/spec/e2e/modules/e2e_bidderSettings.spec.js b/test/spec/e2e/modules/e2e_bidderSettings.spec.js index f8aedfea652..46251d39be3 100644 --- a/test/spec/e2e/modules/e2e_bidderSettings.spec.js +++ b/test/spec/e2e/modules/e2e_bidderSettings.spec.js @@ -26,8 +26,8 @@ setupTest({ waitFor: CREATIVE_IFRAME_CSS_SELECTOR, expectGAMCreative: true }, 'Prebid.js Bidder Settings Ad Unit Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.pbjs.getAdserverTargeting('/19968336/prebid_native_example_2'); }); diff --git a/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js b/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js index 5b5ea2ef2cd..d1803c9784a 100644 --- a/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js +++ b/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js @@ -1,4 +1,4 @@ -/** +/* TODO: old CMP no longer works; see if we can fix this with https://github.com/prebid/Prebid.js/issues/6377 const expect = require('chai').expect; const { testPageURL, switchFrame, waitForElement } = require('../../../helpers/testing-utils'); @@ -59,4 +59,4 @@ describe('Prebid.js GDPR Ad Unit Test', function () { expect(ele.isExisting()).to.be.true; }); }); -**/ + */ diff --git a/test/spec/e2e/modules/e2e_currency.spec.js b/test/spec/e2e/modules/e2e_currency.spec.js index e4eeeab4f5e..865c24cbeb1 100644 --- a/test/spec/e2e/modules/e2e_currency.spec.js +++ b/test/spec/e2e/modules/e2e_currency.spec.js @@ -13,8 +13,8 @@ setupTest({ waitFor: CREATIVE_IFRAME_CSS_SELECTOR, expectGAMCreative: true }, 'Prebid.js Currency Ad Unit Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.pbjs.getAdserverTargeting('/19968336/prebid_native_example_2'); }); diff --git a/test/spec/e2e/multi-bidder/e2e_multiple_bidders.spec.js b/test/spec/e2e/multi-bidder/e2e_multiple_bidders.spec.js index ef34cdc98f1..098dee3647d 100644 --- a/test/spec/e2e/multi-bidder/e2e_multiple_bidders.spec.js +++ b/test/spec/e2e/multi-bidder/e2e_multiple_bidders.spec.js @@ -26,8 +26,8 @@ setupTest({ waitFor: CREATIVE_BANNER_CSS_SELECTOR, expectGAMCreative: true, }, 'Prebid.js Multiple Bidder Ad Unit Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.pbjs.getAdserverTargeting('div-banner-native-2'); }); diff --git a/test/spec/e2e/native/basic_native_ad.spec.js b/test/spec/e2e/native/basic_native_ad.spec.js index 4167046b553..ded7ba610f2 100644 --- a/test/spec/e2e/native/basic_native_ad.spec.js +++ b/test/spec/e2e/native/basic_native_ad.spec.js @@ -26,8 +26,8 @@ setupTest({ waitFor: CREATIVE_IFRAME_CSS_SELECTOR, expectGAMCreative: true }, 'Prebid.js Native Ad Unit Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.pbjs.getAdserverTargeting('/19968336/prebid_native_example_2'); }); diff --git a/test/spec/e2e/outstream/basic_outstream_video_ad.spec.js b/test/spec/e2e/outstream/basic_outstream_video_ad.spec.js index 427839fa92a..4b5c8566f28 100644 --- a/test/spec/e2e/outstream/basic_outstream_video_ad.spec.js +++ b/test/spec/e2e/outstream/basic_outstream_video_ad.spec.js @@ -19,8 +19,8 @@ setupTest({ url: TEST_PAGE_URL, waitFor: CREATIVE_IFRAME_CSS_SELECTOR, }, 'Prebid.js Outstream Video Ad Test', function () { - it('should load the targeting keys with correct values', function () { - const result = browser.execute(function () { + it('should load the targeting keys with correct values', async function () { + const result = await browser.execute(function () { return window.pbjs.getAdserverTargeting('video_ad_unit_2'); }); @@ -30,13 +30,13 @@ setupTest({ expect(targetingKeys.hb_adid_appnexus).to.be.a('string'); }); - it('should render the video ad on the page', function() { + it('should render the video ad on the page', async function() { // skipping test in Edge due to wdio bug: https://github.com/webdriverio/webdriverio/issues/3880 // the iframe for the video does not have a name property and id is generated automatically... if (browser.capabilities.browserName !== 'edge') { - switchFrame(CREATIVE_IFRAME_CSS_SELECTOR); - const ele = $('body > div[id*="an_video_ad_player"] > video'); - expect(ele.isExisting()).to.be.true; + await switchFrame(CREATIVE_IFRAME_CSS_SELECTOR); + const existing = await $('body > div[id*="an_video_ad_player"] > video').isExisting(); + expect(existing).to.be.true; } }); }); diff --git a/test/spec/fpd/enrichment_spec.js b/test/spec/fpd/enrichment_spec.js index 3b3afb15f8c..80ee0dd6cd2 100644 --- a/test/spec/fpd/enrichment_spec.js +++ b/test/spec/fpd/enrichment_spec.js @@ -3,7 +3,10 @@ import {hook} from '../../../src/hook.js'; import {expect} from 'chai/index.mjs'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; +import * as activities from 'src/activities/rules.js' import {CLIENT_SECTIONS} from '../../../src/fpd/oneClient.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../../../src/activities/activities.js'; +import {ACTIVITY_PARAM_COMPONENT} from '../../../src/activities/params.js'; describe('FPD enrichment', () => { let sandbox; @@ -183,6 +186,21 @@ describe('FPD enrichment', () => { }); }); + describe('ext.webdriver', () => { + it('when navigator.webdriver is available', () => { + win.navigator.webdriver = true; + return fpd().then(ortb2 => { + expect(ortb2.device.ext?.webdriver).to.eql(true); + }); + }); + + it('when navigator.webdriver is not present', () => { + return fpd().then(ortb2 => { + expect(ortb2.device.ext?.webdriver).to.not.exist; + }); + }); + }); + it('sets ua', () => { win.navigator.userAgent = 'mock-ua'; return fpd().then(ortb2 => { @@ -213,7 +231,7 @@ describe('FPD enrichment', () => { ua: 'ua' }) }) - }) + }); }); }); @@ -310,6 +328,71 @@ describe('FPD enrichment', () => { }); }); + describe('privacy sandbox cookieDeprecationLabel', () => { + let isAllowed, cdep, shouldCleanupNav = false; + + before(() => { + if (!navigator.cookieDeprecationLabel) { + navigator.cookieDeprecationLabel = {}; + shouldCleanupNav = true; + } + }); + + after(() => { + if (shouldCleanupNav) { + delete navigator.cookieDeprecationLabel; + } + }); + + beforeEach(() => { + isAllowed = true; + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_ACCESS_DEVICE && params[ACTIVITY_PARAM_COMPONENT] === 'prebid.cdep') { + return isAllowed; + } else { + throw new Error('Unexpected activity check'); + } + }); + sandbox.stub(window.navigator, 'cookieDeprecationLabel').value({ + getValue: sinon.stub().callsFake(() => cdep) + }) + }) + + it('enrichment sets device.ext.cdep when allowed and navigator.getCookieDeprecationLabel exists', () => { + cdep = Promise.resolve('example-test-label'); + return fpd().then(ortb2 => { + expect(ortb2.device.ext.cdep).to.eql('example-test-label'); + }) + }); + + Object.entries({ + 'not allowed'() { + isAllowed = false; + }, + 'not supported'() { + delete navigator.cookieDeprecationLabel + } + }).forEach(([t, setup]) => { + it(`if ${t}, the navigator API is not called and no enrichment happens`, () => { + setup(); + cdep = Promise.resolve('example-test-label'); + return fpd().then(ortb2 => { + expect(ortb2.device.ext?.cdep).to.not.exist; + if (navigator.cookieDeprecationLabel) { + sinon.assert.notCalled(navigator.cookieDeprecationLabel.getValue); + } + }) + }); + }) + + it('if the navigator API returns a promise that rejects, the enrichment does not halt forever', () => { + cdep = Promise.reject(new Error('oops, something went wrong')); + return fpd().then(ortb2 => { + expect(ortb2.device.ext?.cdep).to.not.exist; + }) + }); + }); + it('leaves only one of app, site, dooh', () => { return fpd({ app: {p: 'val'}, diff --git a/test/spec/libraries/cmp/cmpClient_spec.js b/test/spec/libraries/cmp/cmpClient_spec.js index 56dd8e12605..adbbbf5cb1d 100644 --- a/test/spec/libraries/cmp/cmpClient_spec.js +++ b/test/spec/libraries/cmp/cmpClient_spec.js @@ -1,4 +1,4 @@ -import {cmpClient} from '../../../../libraries/cmp/cmpClient.js'; +import {cmpClient, MODE_CALLBACK, MODE_RETURN} from '../../../../libraries/cmp/cmpClient.js'; describe('cmpClient', () => { function mockWindow(props = {}) { @@ -7,6 +7,9 @@ describe('cmpClient', () => { addEventListener: sinon.stub().callsFake((evt, listener) => { evt === 'message' && listeners.push(listener) }), + removeEventListener: sinon.stub().callsFake((evt, listener) => { + evt === 'message' && (listeners = listeners.filter((l) => l !== listener)); + }), postMessage: sinon.stub().callsFake((msg) => { listeners.forEach(ln => ln({data: msg})) }), @@ -62,10 +65,15 @@ describe('cmpClient', () => { return 'val' }) }) + Object.entries({ callback: [sinon.stub(), 'undefined', undefined], - 'no callback': [undefined, 'api return value', 'val'] - }).forEach(([t, [callback, tResult, expectedResult]]) => { + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'api return value', 'val', MODE_RETURN], + 'no callback': [undefined, 'api return value', 'val'], + 'no callback, mode = MODE_CALLBACK': [undefined, 'callback arg', 'cbVal', MODE_CALLBACK], + 'no callback, mode = MODE_RETURN': [undefined, 'api return value', 'val', MODE_RETURN], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { describe(`when ${t} is provided`, () => { Object.entries({ 'no success flag': undefined, @@ -73,23 +81,36 @@ describe('cmpClient', () => { }).forEach(([t, success]) => { it(`resolves to ${tResult} (${t})`, (done) => { cbResult = ['cbVal', success]; - mkClient()({callback}).then((val) => { + mkClient({mode})({callback}).then((val) => { expect(val).to.equal(expectedResult); done(); }) + }); + + it('should pass either a function or undefined as callback', () => { + mkClient({mode})({callback}); + sinon.assert.calledWith(mockApiFn, sinon.match.any, sinon.match(arg => typeof arg === 'undefined' || typeof arg === 'function')) }) }); }) }); - it('rejects to undefined when callback is provided and success = false', () => { + it('rejects to undefined when callback is provided and success = false', (done) => { cbResult = ['cbVal', false]; mkClient()({callback: sinon.stub()}).catch(val => { - expect(val).to.equal('cbVal'); + expect(val).to.not.exist; done(); }) }); + it('rejects to callback arg when callback is NOT provided, success = false, mode = MODE_CALLBACK', (done) => { + cbResult = ['cbVal', false]; + mkClient({mode: MODE_CALLBACK})().catch(val => { + expect(val).to.eql('cbVal'); + done(); + }) + }) + it('rejects when CMP api throws', (done) => { mockApiFn.reset(); const e = new Error(); @@ -98,7 +119,7 @@ describe('cmpClient', () => { expect(val).to.equal(e); done(); }); - }) + }); }) it('should use apiArgs to choose and order the arguments to pass to the API fn', () => { @@ -109,6 +130,10 @@ describe('cmpClient', () => { }); sinon.assert.calledWith(mockApiFn, 'mockParam', 'mockCmd'); }); + + it('should not choke on .close()', () => { + mkClient({}).close(); + }) }) }) }) @@ -189,8 +214,12 @@ describe('cmpClient', () => { }) Object.entries({ 'callback': [sinon.stub(), 'undefined', undefined], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'undefined', undefined, MODE_RETURN], + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], 'no callback': [undefined, 'response returnValue', 'val'], - }).forEach(([t, [callback, tResult, expectedResult]]) => { + 'no callback, mode = MODE_RETURN': [undefined, 'undefined', undefined, MODE_RETURN], + 'no callback, mode = MODE_CALLBACK': [undefined, 'response returnValue', 'val', MODE_CALLBACK], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { describe(`when ${t} is provided`, () => { Object.entries({ 'no success flag': {}, @@ -198,35 +227,69 @@ describe('cmpClient', () => { }).forEach(([t, resp]) => { it(`resolves to ${tResult} (${t})`, () => { Object.assign(response, resp); - mkClient()({callback}).then((val) => { + mkClient({mode})({callback}).then((val) => { expect(val).to.equal(expectedResult); }) }) }); - it(`rejects to ${tResult} when success = false`, (done) => { - response.success = false; - mkClient()({callback}).catch((err) => { - expect(err).to.equal(expectedResult); - done(); + if (mode !== MODE_RETURN) { // in return mode, the promise never rejects + it(`rejects to ${tResult} when success = false`, (done) => { + response.success = false; + mkClient()({mode, callback}).catch((err) => { + expect(err).to.equal(expectedResult); + done(); + }); }); - }); + } }) }); }); - it('should re-use callback for messages with same callId', () => { - messenger.reset(); - let callId; - messenger.callsFake((msg) => { if (msg.mockApiCall) callId = msg.mockApiCall.callId }); - const callback = sinon.stub(); - mkClient()({callback}); - expect(callId).to.exist; - win.postMessage({mockApiReturn: {callId, returnValue: 'a'}}); - win.postMessage({mockApiReturn: {callId, returnValue: 'b'}}); - sinon.assert.calledWith(callback, 'a'); - sinon.assert.calledWith(callback, 'b'); - }) + describe('messages with same callID', () => { + let callback, callId; + + function runCallback(returnValue) { + win.postMessage({mockApiReturn: {callId, returnValue}}); + } + + beforeEach(() => { + callId = null; + messenger.reset(); + messenger.callsFake((msg) => { + if (msg.mockApiCall) callId = msg.mockApiCall.callId; + }); + callback = sinon.stub(); + }); + + it('should re-use callback for messages with same callId', () => { + mkClient()({callback}); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledWith(callback, 'b'); + }); + + it('should NOT re-use callback if once = true', () => { + mkClient()({callback}, true); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }); + + it('should NOT fire again after .close()', () => { + const client = mkClient(); + client({callback}); + runCallback('a'); + client.close(); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }) + }); }); }); }); diff --git a/test/spec/libraries/currencyUtils_spec.js b/test/spec/libraries/currencyUtils_spec.js new file mode 100644 index 00000000000..9d3d73e6a5f --- /dev/null +++ b/test/spec/libraries/currencyUtils_spec.js @@ -0,0 +1,113 @@ +import {getGlobal} from 'src/prebidGlobal.js'; +import {convertCurrency, currencyCompare, currencyNormalizer} from 'libraries/currencyUtils/currency.js'; + +describe('currency utils', () => { + let sandbox; + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }) + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('convertCurrency', () => { + Object.entries({ + 'not available': () => sandbox.stub(getGlobal(), 'convertCurrency').value(undefined), + 'throwing errors': () => sandbox.stub(getGlobal(), 'convertCurrency').callsFake(() => { throw new Error(); }), + }).forEach(([t, setup]) => { + describe(`when currency module is ${t}`, () => { + beforeEach(setup); + + it('should "convert" to the same currency', () => { + expect(convertCurrency(123, 'mock', 'mock', false)).to.eql(123); + }); + + it('should throw when suppressErrors = false', () => { + expect(() => convertCurrency(123, 'c1', 'c2', false)).to.throw(); + }); + + it('should return input value when suppressErrors = true', () => { + expect(convertCurrency(123, 'c1', 'c2', true)).to.eql(123); + }) + }) + }); + + describe('when currency module is working', () => { + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amt) => amt * 10) + }); + + it('should be used for actual conversions', () => { + expect(convertCurrency(123, 'c1', 'c2')).to.eql(1230); + sinon.assert.calledWith(getGlobal().convertCurrency, 123, 'c1', 'c2'); + }); + + it('should NOT be used when no conversion is necessary', () => { + expect(convertCurrency(123, 'cur', 'cur')).to.eql(123); + sinon.assert.notCalled(getGlobal().convertCurrency); + }) + }) + }); + + describe('Currency normalization', () => { + let mockConvert; + beforeEach(() => { + mockConvert = sinon.stub().callsFake((amt, from, to) => { + if (from === to) return amt; + return amt / from * to + }) + }); + + describe('currencyNormalizer', () => { + it('converts to toCurrency if set', () => { + const normalize = currencyNormalizer(10, true, mockConvert); + expect(normalize(1, 1)).to.eql(10); + expect(normalize(10, 100)).to.eql(1); + }); + + it('converts to first currency if toCurrency is not set', () => { + const normalize = currencyNormalizer(null, true, mockConvert); + expect(normalize(1, 1)).to.eql(1); + expect(normalize(1, 10)).to.eql(0.1); + }); + + [true, false].forEach(bestEffort => { + it(`passes bestEffort = ${bestEffort} to convert`, () => { + currencyNormalizer(null, bestEffort, mockConvert)(1, 1); + sinon.assert.calledWith(mockConvert, 1, 1, 1, bestEffort); + }) + }) + }); + + describe('currencyCompare', () => { + let compare + beforeEach(() => { + compare = currencyCompare((val) => [val.amount, val.cur], currencyNormalizer(null, false, mockConvert)) + }); + [ + [{amount: 1, cur: 1}, {amount: 1, cur: 10}, 1], + [{amount: 10, cur: 1}, {amount: 0.1, cur: 100}, 1], + [{amount: 1, cur: 1}, {amount: 10, cur: 10}, 0], + ].forEach(([a, b, expected]) => { + it(`should compare ${a.amount}/${a.cur} and ${b.amount}/${b.cur}`, () => { + expect(compare(a, b)).to.equal(expected); + expect(compare(b, a)).to.equal(-expected); + }); + }); + }) + }) +}) diff --git a/test/spec/libraries/mspa/activityControls_spec.js b/test/spec/libraries/mspa/activityControls_spec.js index 5286f1d47f0..f232dc2563f 100644 --- a/test/spec/libraries/mspa/activityControls_spec.js +++ b/test/spec/libraries/mspa/activityControls_spec.js @@ -1,220 +1,159 @@ -import {mspaRule, setupRules, isTransmitUfpdConsentDenied, isTransmitGeoConsentDenied, isBasicConsentDenied, isSensitiveNoticeMissing, isConsentDenied} from '../../../../libraries/mspa/activityControls.js'; +import {mspaRule, setupRules, isTransmitUfpdConsentDenied, isTransmitGeoConsentDenied, isBasicConsentDenied, sensitiveNoticeIs, isConsentDenied} from '../../../../libraries/mspa/activityControls.js'; import {ruleRegistry} from '../../../../src/activities/rules.js'; -describe('isBasicConsentDenied', () => { - const cd = { - // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo - Gpc: 0, - KnownChildSensitiveDataConsents: [0, 0], - MspaCoveredTransaction: 2, - MspaOptOutOptionMode: 0, - MspaServiceProviderMode: 0, - PersonalDataConsents: 0, - SaleOptOut: 2, - SaleOptOutNotice: 1, - SensitiveDataLimitUseNotice: 1, - SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], - SensitiveDataProcessingOptOutNotice: 1, - SharingNotice: 1, - SharingOptOut: 2, - SharingOptOutNotice: 1, - TargetedAdvertisingOptOut: 2, - TargetedAdvertisingOptOutNotice: 1, - Version: 1 - }; - it('should be false (basic consent conditions pass) with variety of notice and opt in', () => { - const result = isBasicConsentDenied(cd); - expect(result).to.equal(false); - }); - it('should be true (basic consent conditions do not pass) with personal data consent set to true (invalid state)', () => { - cd.PersonalDataConsents = 2; - const result = isBasicConsentDenied(cd); - expect(result).to.equal(true); - cd.PersonalDataConsents = 0; - }); - it('should be true (basic consent conditions do not pass) with sensitive opt in but no notice', () => { - cd.SensitiveDataLimitUseNotice = 0; - const result = isBasicConsentDenied(cd); - expect(result).to.equal(true); - cd.SensitiveDataLimitUseNotice = 1; - }); -}) -describe('isSensitiveNoticeMissing', () => { - const cd = { - // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo - Gpc: 0, - KnownChildSensitiveDataConsents: [0, 0], - MspaCoveredTransaction: 2, - MspaOptOutOptionMode: 0, - MspaServiceProviderMode: 0, - PersonalDataConsents: 0, - SaleOptOut: 2, - SaleOptOutNotice: 1, - SensitiveDataLimitUseNotice: 1, - SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], - SensitiveDataProcessingOptOutNotice: 1, - SharingNotice: 1, - SharingOptOut: 2, - SharingOptOutNotice: 1, - TargetedAdvertisingOptOut: 2, - TargetedAdvertisingOptOutNotice: 1, - Version: 1 - }; - it('should be false (sensitive notice is given or not needed) with variety of notice and opt in', () => { - const result = isSensitiveNoticeMissing(cd); - expect(result).to.equal(false); - }); - it('should be true (sensitive notice is missing) with variety of notice and opt in', () => { - cd.SensitiveDataLimitUseNotice = 2; - const result = isSensitiveNoticeMissing(cd); - expect(result).to.equal(true); - cd.SensitiveDataLimitUseNotice = 1; - }); -}) -describe('isConsentDenied', () => { - const cd = { - // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo - Gpc: 0, - KnownChildSensitiveDataConsents: [0, 0], - MspaCoveredTransaction: 2, - MspaOptOutOptionMode: 0, - MspaServiceProviderMode: 0, - PersonalDataConsents: 0, - SaleOptOut: 2, - SaleOptOutNotice: 1, - SensitiveDataLimitUseNotice: 1, - SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], - SensitiveDataProcessingOptOutNotice: 1, - SharingNotice: 1, - SharingOptOut: 2, - SharingOptOutNotice: 1, - TargetedAdvertisingOptOut: 2, - TargetedAdvertisingOptOutNotice: 1, - Version: 1 - }; - it('should be false (consent given personalized ads / sale / share) with variety of notice and opt in', () => { - const result = isConsentDenied(cd); - expect(result).to.equal(false); - }); - it('should be true (no consent) on opt out of targeted ads via TargetedAdvertisingOptOut', () => { - cd.TargetedAdvertisingOptOut = 1; - const result = isConsentDenied(cd); - expect(result).to.equal(true); - cd.TargetedAdvertisingOptOut = 2; - }); - it('should be true (no consent) on opt out of targeted ads via no TargetedAdvertisingOptOutNotice', () => { - cd.TargetedAdvertisingOptOutNotice = 2; - const result = isConsentDenied(cd); - expect(result).to.equal(true); - cd.TargetedAdvertisingOptOutNotice = 1; - }); - it('should be true (no consent) if TargetedAdvertisingOptOutNotice is 0 and TargetedAdvertisingOptOut is 2', () => { - cd.TargetedAdvertisingOptOutNotice = 0; - const result = isConsentDenied(cd); - expect(result).to.equal(true); - cd.TargetedAdvertisingOptOutNotice = 1; - }); -}) -describe('isTransmitUfpdConsentDenied', () => { - const cd = { - // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo - Gpc: 0, - KnownChildSensitiveDataConsents: [0, 0], - MspaCoveredTransaction: 2, - MspaOptOutOptionMode: 0, - MspaServiceProviderMode: 0, - PersonalDataConsents: 0, - SaleOptOut: 2, - SaleOptOutNotice: 1, - SensitiveDataLimitUseNotice: 1, - SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], - SensitiveDataProcessingOptOutNotice: 1, - SharingNotice: 1, - SharingOptOut: 2, - SharingOptOutNotice: 1, - TargetedAdvertisingOptOut: 2, - TargetedAdvertisingOptOutNotice: 1, - Version: 1 - }; - it('should be false (consent given to add ufpd) with variety of notice and opt in', () => { - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(false); - }); - it('should be true (consent denied to add ufpd) if no consent to process health information', () => { - cd.SensitiveDataProcessing[2] = 1; - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(true); - cd.SensitiveDataProcessing[2] = 0; - }); - it('should be true (consent denied to add ufpd) with consent to process biometric data, as this should not be on openrtb', () => { - cd.SensitiveDataProcessing[6] = 1; - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(true); - cd.SensitiveDataProcessing[6] = 1; - }); - it('should be true (consent denied to add ufpd) without sharing notice', () => { - cd.SharingNotice = 2; - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(true); - cd.SharingNotice = 1; - }); - it('should be true (consent denied to add ufpd) with sale opt out', () => { - cd.SaleOptOut = 1; - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(true); - cd.SaleOptOut = 2; - }); - it('should be true (consent denied to add ufpd) without targeted ads opt out', () => { - cd.TargetedAdvertisingOptOut = 1; - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(true); - cd.TargetedAdvertisingOptOut = 2; - }); - it('should be true (consent denied to add ufpd) with missing sensitive data limit notice', () => { - cd.SensitiveDataLimitUseNotice = 2; - const result = isTransmitUfpdConsentDenied(cd); - expect(result).to.equal(true); - cd.SensitiveDataLimitUseNotice = 1; - }); -}) -describe('isTransmitGeoConsentDenied', () => { - const cd = { - // not covered, opt out of geo - Gpc: 0, - KnownChildSensitiveDataConsents: [0, 0], - MspaCoveredTransaction: 2, - MspaOptOutOptionMode: 0, - MspaServiceProviderMode: 0, - PersonalDataConsents: 0, - SaleOptOut: 2, - SaleOptOutNotice: 1, - SensitiveDataLimitUseNotice: 1, - SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], - SensitiveDataProcessingOptOutNotice: 1, - SharingNotice: 1, - SharingOptOut: 2, - SharingOptOutNotice: 1, - TargetedAdvertisingOptOut: 2, - TargetedAdvertisingOptOutNotice: 1, - Version: 1 - }; - it('should be true (consent denied to add precise geo) -- sensitive flag denied', () => { - const result = isTransmitGeoConsentDenied(cd); - expect(result).to.equal(true); +describe('Consent interpretation', () => { + function mkConsent(flags) { + return Object.assign({ + // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo + Gpc: 0, + KnownChildSensitiveDataConsents: [0, 0], + MspaCoveredTransaction: 2, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, + PersonalDataConsents: 0, + SaleOptOut: 2, + SaleOptOutNotice: 1, + SensitiveDataLimitUseNotice: 1, + SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], + SensitiveDataProcessingOptOutNotice: 1, + SharingNotice: 1, + SharingOptOut: 2, + SharingOptOutNotice: 1, + TargetedAdvertisingOptOut: 2, + TargetedAdvertisingOptOutNotice: 1, + Version: 1 + }, flags) + } + describe('isBasicConsentDenied', () => { + it('should be false (basic consent conditions pass) with variety of notice and opt in', () => { + const result = isBasicConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + it('should be true (basic consent conditions do not pass) with personal data consent set to true (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + PersonalDataConsents: 2 + })); + expect(result).to.equal(true); + }); + it('should be true (basic consent conditions do not pass) with covered set to zero (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + MspaCoveredTransaction: 0 + })); + expect(result).to.equal(true); + }); + it('should not deny when consent for under-13 is null', () => { + expect(isBasicConsentDenied(mkConsent({ + KnownChildSensitiveDataConsents: [0, null] + }))).to.be.false; + }) }); - it('should be true (consent denied to add precise geo) -- sensitive data limit usage not given', () => { - cd.SensitiveDataLimitUseNotice = 0; - const result = isTransmitGeoConsentDenied(cd); - expect(result).to.equal(true); - cd.SensitiveDataLimitUseNotice = 1; + + describe('isConsentDenied', () => { + it('should be false (consent given personalized ads / sale / share) with variety of notice and opt in', () => { + const result = isConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + it('should be true (no consent) on opt out of targeted ads via TargetedAdvertisingOptOut', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOut: 1 + })); + expect(result).to.equal(true); + }); + it('should be true (no consent) on opt out of targeted ads via no TargetedAdvertisingOptOutNotice', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOutNotice: 2 + })); + expect(result).to.equal(true); + }); + it('should be true (no consent) if TargetedAdvertisingOptOutNotice is 0 and TargetedAdvertisingOptOut is 2', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOutNotice: 0 + })); + expect(result).to.equal(true); + }); + it('requires also SharingNotice to accept opt-in for Sharing', () => { + expect(isConsentDenied(mkConsent({ + SharingNotice: 0 + }))).to.be.true; + }) }); - it('should be false (consent given to add precise geo) -- sensitive position 8 (index 7) is true', () => { - cd.SensitiveDataProcessing[7] = 2; - const result = isTransmitGeoConsentDenied(cd); - expect(result).to.equal(false); - cd.SensitiveDataProcessing[7] = 1; + + describe('isTransmitUfpdConsentDenied', () => { + it('should be false (consent given to add ufpd) with variety of notice and opt in', () => { + const result = isTransmitUfpdConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + Object.entries({ + 'health information': 2, + 'biometric data': 6, + }).forEach(([t, flagNo]) => { + it(`'should be true (consent denied to add ufpd) if no consent to process ${t}'`, () => { + const consent = mkConsent(); + consent.SensitiveDataProcessing[flagNo] = 1; + expect(isTransmitUfpdConsentDenied(consent)).to.be.true; + }) + }); + + ['SharingNotice', 'SensitiveDataLimitUseNotice'].forEach(flag => { + it(`should be true (consent denied to add ufpd) without ${flag}`, () => { + expect(isTransmitUfpdConsentDenied(mkConsent({ + [flag]: 2 + }))).to.be.true; + }) + }); + + ['SaleOptOut', 'TargetedAdvertisingOptOut'].forEach(flag => { + it(`should be true (consent denied to add ufpd) with ${flag}`, () => { + expect(isTransmitUfpdConsentDenied(mkConsent({ + [flag]: 1 + }))).to.be.true; + }) + }); + + it('should be true (basic consent conditions do not pass) with sensitive opt in but no notice', () => { + const cd = mkConsent({ + SensitiveDataLimitUseNotice: 0 + }); + cd.SensitiveDataProcessing[0] = 2; + expect(isTransmitUfpdConsentDenied(cd)).to.be.true; + }); + + it('should deny when sensitive notice is missing', () => { + const result = isTransmitUfpdConsentDenied(mkConsent({ + SensitiveDataLimitUseNotice: 2 + })); + expect(result).to.equal(true); + }); + + it('should not deny when biometric data opt-out is null', () => { + const cd = mkConsent(); + cd.SensitiveDataProcessing[6] = null; + expect(isTransmitUfpdConsentDenied(cd)).to.be.false; + }) }); -}) + + describe('isTransmitGeoConsentDenied', () => { + function geoConsent(geoOptOut, flags) { + const consent = mkConsent(flags); + consent.SensitiveDataProcessing[7] = geoOptOut; + return consent; + } + it('should be true (consent denied to add precise geo) -- sensitive flag denied', () => { + const result = isTransmitGeoConsentDenied(geoConsent(1)); + expect(result).to.equal(true); + }); + it('should be true (consent denied to add precise geo) -- sensitive data limit usage not given', () => { + const result = isTransmitGeoConsentDenied(geoConsent(1, { + SensitiveDataLimitUseNotice: 0 + })); + expect(result).to.equal(true); + }); + it('should be false (consent given to add precise geo) -- sensitive position 8 (index 7) is true', () => { + const result = isTransmitGeoConsentDenied(geoConsent(2)); + expect(result).to.equal(false); + }); + }) +}); describe('mspaRule', () => { it('does not apply if SID is not applicable', () => { @@ -270,10 +209,12 @@ describe('setupRules', () => { ([registerRule, isAllowed] = ruleRegistry()); consent = { applicableSections: [1], - sectionData: { - mockApi: { - mock: 'consent' - } + parsedSections: { + mockApi: [ + { + mock: 'consent' + } + ] } }; }); @@ -282,7 +223,7 @@ describe('setupRules', () => { return setupRules(api, sids, normalize, rules, registerRule, () => consent) } - it('should use section data for the given api', () => { + it('should use flatten section data for the given api', () => { runSetup('mockApi', [1]); expect(isAllowed('mockActivity', {})).to.equal(false); sinon.assert.calledWith(rules.mockActivity, {mock: 'consent'}) @@ -299,7 +240,7 @@ describe('setupRules', () => { expect(isAllowed('mockActivity', {})).to.equal(true); }); - it('should pass consent through normalizeConsent', () => { + it('should pass flattened consent through normalizeConsent', () => { const normalize = sinon.stub().returns({normalized: 'consent'}) runSetup('mockApi', [1], normalize); expect(isAllowed('mockActivity', {})).to.equal(false); diff --git a/test/spec/libraries/sizeUtils_spec.js b/test/spec/libraries/sizeUtils_spec.js new file mode 100644 index 00000000000..1c954c6accf --- /dev/null +++ b/test/spec/libraries/sizeUtils_spec.js @@ -0,0 +1,30 @@ +import {getAdUnitSizes} from '../../../libraries/sizeUtils/sizeUtils.js'; +import {expect} from 'chai/index.js'; + +describe('getAdUnitSizes', function () { + it('returns an empty response when adUnits is undefined', function () { + let sizes = getAdUnitSizes(); + expect(sizes).to.be.undefined; + }); + + it('returns an empty array when invalid data is present in adUnit object', function () { + let sizes = getAdUnitSizes({sizes: 300}); + expect(sizes).to.deep.equal([]); + }); + + it('retuns an array of arrays when reading from adUnit.sizes', function () { + let sizes = getAdUnitSizes({sizes: [300, 250]}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({sizes: [[300, 250], [300, 600]]}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); + + it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { + let sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [300, 250]}}}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); +}); diff --git a/test/spec/libraries/urlUtils_spec.js b/test/spec/libraries/urlUtils_spec.js new file mode 100644 index 00000000000..9dd66b05407 --- /dev/null +++ b/test/spec/libraries/urlUtils_spec.js @@ -0,0 +1,24 @@ +import {tryAppendQueryString} from '../../../libraries/urlUtils/urlUtils.js'; +import assert from 'assert'; + +describe('tryAppendQueryString', function () { + it('should append query string to existing url', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = 'c'; + + var output = tryAppendQueryString(url, key, value); + + var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; + assert.equal(output, expectedResult); + }); + + it('should return existing url, if the value is empty', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = ''; + + var output = tryAppendQueryString(url, key, value); + assert.equal(output, url); + }); +}); diff --git a/test/spec/libraries/vastTrackers_spec.js b/test/spec/libraries/vastTrackers_spec.js new file mode 100644 index 00000000000..3849ea75b02 --- /dev/null +++ b/test/spec/libraries/vastTrackers_spec.js @@ -0,0 +1,33 @@ +import {addImpUrlToTrackers, getVastTrackers, insertVastTrackers, registerVastTrackers} from 'libraries/vastTrackers/vastTrackers.js'; +import {MODULE_TYPE_ANALYTICS} from '../../../src/activities/modules.js'; + +describe('vast trackers', () => { + it('insert into tracker list', function() { + let trackers = getVastTrackers({'cpm': 1.0}); + if (!trackers || !trackers.get('impressions')) { + registerVastTrackers(MODULE_TYPE_ANALYTICS, 'test', function(bidResponse) { + return [ + {'event': 'impressions', 'url': `https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`} + ]; + }); + } + trackers = getVastTrackers({'cpm': 1.0}); + expect(trackers).to.be.a('map'); + expect(trackers.get('impressions')).to.exists; + expect(trackers.get('impressions').has('https://vasttracking.mydomain.com/vast?cpm=1')).to.be.true; + }); + + it('insert trackers in vastXml', function() { + const trackers = getVastTrackers({'cpm': 1.0}); + let vastXml = ''; + vastXml = insertVastTrackers(trackers, vastXml); + expect(vastXml).to.equal(''); + }); + + it('test addImpUrlToTrackers', function() { + const trackers = addImpUrlToTrackers({'vastImpUrl': 'imptracker.com'}, getVastTrackers({'cpm': 1.0})); + expect(trackers).to.be.a('map'); + expect(trackers.get('impressions')).to.exists; + expect(trackers.get('impressions').has('imptracker.com')).to.be.true; + }); +}) diff --git a/test/spec/modules/33acrossAnalyticsAdapter_spec.js b/test/spec/modules/33acrossAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9e0d928cd97 --- /dev/null +++ b/test/spec/modules/33acrossAnalyticsAdapter_spec.js @@ -0,0 +1,1163 @@ +// @ts-nocheck +import analyticsAdapter from 'modules/33acrossAnalyticsAdapter.js'; +import { log } from 'modules/33acrossAnalyticsAdapter.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; +import * as events from 'src/events.js'; +import * as faker from 'faker'; +import CONSTANTS from 'src/constants.json'; +import { gdprDataHandler, gppDataHandler, uspDataHandler } from '../../../src/adapterManager'; +import { DEFAULT_ENDPOINT, POST_GAM_TIMEOUT, locals } from '../../../modules/33acrossAnalyticsAdapter'; +const { EVENTS, BID_STATUS } = CONSTANTS; + +describe('33acrossAnalyticsAdapter:', function () { + let sandbox; + let assert = getLocalAssert(); + + beforeEach(function () { + mockGpt.reset(); + + sandbox = sinon.createSandbox({ + useFakeTimers: { + now: new Date(2023, 3, 3, 0, 1, 33, 425), + }, + }); + + sandbox.stub(events, 'getEvents').returns([]); + + sandbox.spy(log, 'info'); + sandbox.spy(log, 'warn'); + sandbox.spy(log, 'error'); + + sandbox.stub(navigator, 'sendBeacon').callsFake(function (url, data) { + const json = JSON.parse(data); + assert.isValidAnalyticsReport(json); + + return true; + }); + }); + + afterEach(function () { + analyticsAdapter.disableAnalytics(); + mockGpt.enable(); + sandbox.restore(); + }); + + describe('enableAnalytics:', function () { + context('When pid is given', function () { + context('but endpoint is not', function () { + it('uses the default endpoint', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + }, + }); + + assert.equal(analyticsAdapter.getUrl(), DEFAULT_ENDPOINT); + }); + }); + + context('but the endpoint is invalid', function () { + it('logs an info message', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'foo' + }, + }); + + assert.calledWithExactly(log.info, 'Invalid endpoint provided for "options.endpoint". Using default endpoint.'); + }); + }); + }); + + context('When endpoint is given', function () { + context('but pid is not', function () { + it('logs an error message', function () { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: faker.internet.url() + }, + }); + + assert.calledWithExactly(log.error, 'No partnerId provided for "options.pid". No analytics will be sent.'); + }); + }); + }); + + context('When pid and endpoint are given', function () { + context('and an invalid timeout config value is given', function () { + it('logs an info message', function () { + [null, 'foo', -1].forEach(timeout => { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'http://test-endpoint', + timeout + }, + }); + analyticsAdapter.disableAnalytics(); + + assert.calledWithExactly(log.info, 'Invalid timeout provided for "options.timeout". Using default timeout of 10000ms.'); + log.info.resetHistory(); + }); + }); + }); + }); + }); + + // check that upcoming tests are derived from a valid report + describe('Report Mocks', function () { + it('the report should have the correct format', function () { + assert.isValidAnalyticsReport(createReportWithThreeBidWonEvents()); + }); + }); + + describe('Event Handling', function () { + beforeEach(function () { + this.defaultTimeout = 10000; + this.enableAnalytics = (options) => { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: 'http://test-endpoint', + pid: 'test-pid', + timeout: this.defaultTimeout, + ...options + }, + }); + window.googletag.cmd.forEach(cmd => cmd()); + } + }); + + context('when an auction is complete', function () { + context('and the AnalyticsReport is sent successfully to the given endpoint', function () { + it('calls "sendBeacon" with all won bids', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const [url, jsonString] = navigator.sendBeacon.firstCall.args; + const { auctions } = JSON.parse(jsonString); + + assert.lengthOf(mapToBids(auctions).filter(bid => bid.hasWon), 3); + }); + + it('calls "sendBeacon" with the correct report string', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + + it('logs an info message containing the report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())) + .returns(true); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.info, `Analytics report sent to ${endpoint}`, createReportWithThreeBidWonEvents()); + }); + + it('calls "sendBeacon" as soon as all values are available (before timeout)', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('and a valid US Privacy configuration is present', function () { + ['1YNY', '1---', '1NY-', '1Y--', '1--Y', '1N--', '1--N', '1NNN'].forEach(consent => { + it(`calls "sendBeacon" with a report containing the "${consent}" privacy string`, function () { + sandbox.stub(uspDataHandler, 'getConsentData').returns(consent); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + usPrivacy: consent + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('and a GDPR Privacy configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GDPR consent string', function () { + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ + consentString: 'foo', + gdprApplies: true + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gdpr: 1, + gdprConsent: 'foo' + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('when an auction is complete and a GPP configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GPP consent string', function () { + sandbox.stub(gppDataHandler, 'getConsentData').returns({ + gppString: 'gppString', + applicableSections: [7] + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gpp: 'gppString', + gppSid: [7] + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + + context('when an error occurs while sending the AnalyticsReport', function () { + it('logs an error', function () { + this.enableAnalytics(); + navigator.sendBeacon.returns(false); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.error, 'Analytics report exceeded User-Agent data limits and was not sent.', createReportWithThreeBidWonEvents()); + }); + }); + + context('when an auction report was already sent', function () { + context('and a new bid won event is returned after the report completes', function () { + it('finishes the auction without error', function () { + const incompleteAnalyticsReport = createReportWithThreeBidWonEvents(); + incompleteAnalyticsReport.auctions.forEach(auction => { + auction.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + delete bid.bidResponse; + bid.hasWon = 0; + bid.status = 'pending'; + }); + }); + }); + + this.enableAnalytics(); + const { prebid: [auction] } = getMockEvents(); + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + sandbox.clock.tick(this.defaultTimeout + 1000); + + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', incompleteAnalyticsReport); + }); + }); + + context('and another auction completes after that', function () { + it('sends the new report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + }); + + context('when two auctions overlap', function() { + it('sends a report for each auction', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + + context('when an AUCTION_END event is received before BID_WON events', function () { + it('sends a report with the bids that have won after all bids are won', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + const { prebid: [auction] } = getMockEvents(); + + performStandardAuction({ exclude: [EVENTS.BID_WON] }); + + assert.notCalled(navigator.sendBeacon); + for (let bidWon of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWon); + } + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('when a BID_WON event is received', function () { + context('and there is no record of that bid being requested', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const mockEvents = getMockEvents(); + const { prebid } = mockEvents; + const [auction] = prebid; + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + + const fakeBidWonEvent = Object.assign(auction.BID_WON[0], { + transactionId: 'foo' + }) + + events.emit(EVENTS.BID_WON, fakeBidWonEvent); + + const { auctionId, requestId } = fakeBidWonEvent; + assert.calledWithExactly(log.error, `Cannot find bid "${requestId}" in auction "${auctionId}".`); + }); + }); + }); + + context('when a BID_REJECTED event is received', function () { + it(`marks the rejected bid as "rejected"`, function () { + this.enableAnalytics(); + + const auction = getMockEvents().prebid[0]; + + // Start the auction + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + // Reject first bid + const bidToReject = auction.BID_REQUESTED[0].bids[0]; + events.emit(EVENTS.BID_REJECTED, auction.BID_REJECTED[0]); + + // Accept remaining bids + for (let i = 1; i < auction.BID_RESPONSE.length; ++i) { + events.emit(EVENTS.BID_RESPONSE, auction.BID_RESPONSE[i]); + }; + + // Complete the auction + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(this.defaultTimeout + 1); + + // Verify that we detected that the first bid was rejected + const expectedRejectedBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(expectedRejectedBid.status, 'rejected'); + }); + }); + + context('when a transaction does not reach its complete state', function () { + context('and a timeout config value has been given', function () { + context('and the timeout value has elapsed', function () { + it('logs a warning', function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(timeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + + it(`marks timed out bids as "timeout"`, function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + const request = getMockEvents().prebid[0].BID_REQUESTED[0]; + const bidToTimeout = request.bids[0]; + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + sandbox.clock.tick(1); + events.emit(EVENTS.BID_TIMEOUT, [{ + auctionId: request.auctionId, + bidId: bidToTimeout.bidId, + transactionId: bidToTimeout.transactionId, + }]); + sandbox.clock.tick(timeout + 1000); + + const timeoutBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(timeoutBid.status, 'timeout'); + }); + }); + }); + + context('and a timeout config value has not been given', function () { + context('and the default timeout has elapsed', function () { + it('logs an error', function () { + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + }) + }); + + context('and the `slotRenderEnded` event fired for all bids, but not all bids have won', function () { + context('and the GAM slot IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + context('and the slot element IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd'], useSlotElementIds: true}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + it('does NOT send a report if not all `slotRenderEnded` events have timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT - 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 0); + }); + }); + + context('and the `slotRenderEnded` event has fired for an unknown slot code', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + auction.AUCTION_INIT.adUnits[0].code = 'INVALID_AD_UNIT_CODE'; + + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.calledWithExactly(log.warn, + 'Could not find configured ad unit matching GAM render of slot:', + { slotName: `${adUnitCodes[0]} - ${adSlotElementIds[0]}` }); + }); + }); + + context('and the incomplete report has been sent successfully', function () { + it('sends a report string with any bids with rendered status set to hasWon: 1', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 1); + }); + + it('reports bids with only targetingSet status as hasWon: 0', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 0); + }); + + it('logs an info message', function () { + navigator.sendBeacon.returns(true); + + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWith(log.info, `Analytics report sent to ${endpoint}`); + }); + }); + }); + + context('when the transaction manager has open transactions', function () { + it('reports those transactions as pending', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.equal(manager.status().pending.length, auction.BID_REQUESTED[0].bids.length); + }); + + context('and a single bidWon event has triggered', function () { + it('completes the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + events.emit(EVENTS.BID_WON, auction.BID_WON[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + + context('and a single slotRenderEnded event has triggered', function () { + context('and the Google Ad Manager timeout has not elapsed', function () { + it('does NOT complete the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 0, + pending: auction.BID_REQUESTED[0].bids.length + }); + }); + }); + + context('and the Google Ad Manager timeout has elapsed', function () { + it('completes the transaction', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({timeout}); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + }); + }); + }); +}); + +const adUnitCodes = ['/19968336/header-bid-tag-0', '/19968336/header-bid-tag-1', '/17118521/header-bid-tag-2']; +const adSlotElementIds = ['ad-slot-div-0', 'ad-slot-div-1', 'ad-slot-div-2']; + +function performStandardAuction({ exclude = [], useSlotElementIds = false } = {}) { + const mockEvents = getMockEvents(); + const { prebid, gam } = mockEvents; + const [auction] = prebid; + + if (!exclude.includes(EVENTS.AUCTION_INIT)) { + if (useSlotElementIds) { + // With this option, identify the ad units by slot element IDs instead of GAM paths + auction.AUCTION_INIT.adUnits.forEach((adUnit, i) => { + adUnit.code = adSlotElementIds[i]; + }); + } + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + } + + if (!exclude.includes(EVENTS.BID_REQUESTED)) { + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + } + + if (!exclude.includes(EVENTS.BID_RESPONSE)) { + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + } + + if (!exclude.includes(EVENTS.AUCTION_END)) { + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + } + + if (!exclude.includes('slotRenderEnded')) { + for (let gEvent of gam.slotRenderEnded) { + mockGpt.emitEvent('slotRenderEnded', gEvent); + } + } + + if (!exclude.includes(EVENTS.BID_WON)) { + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + } +} + +function mapToBids(auctions) { + return auctions.flatMap( + auction => auction.adUnits.flatMap( + au => au.bids + ) + ); +} + +function getLocalAssert() { + function isValidAnalyticsReport(report) { + assert.containsAllKeys(report, ['analyticsVersion', 'pid', 'src', 'pbjsVersion', 'auctions']); + if ('usPrivacy' in report) { + assert.match(report.usPrivacy, /[0|1][Y|N|-]{3}/); + } + if ('gdpr' in report) { + assert.oneOf(report.gdpr, [0, 1]); + } + if (report.gdpr === 1) { + assert.isString(report.gdprConsent); + } + if ('gpp' in report) { + assert.isString(report.gpp); + assert.isArray(report.gppSid); + } + if ('coppa' in report) { + assert.oneOf(report.coppa, [0, 1]); + } + + assert.equal(report.analyticsVersion, '1.0.0'); + assert.isString(report.pid); + assert.isString(report.src); + assert.equal(report.pbjsVersion, '$prebid.version$'); + assert.isArray(report.auctions); + assert.isAbove(report.auctions.length, 0); + report.auctions.forEach(isValidAuction); + } + function isValidAuction(auction) { + assert.hasAllKeys(auction, ['adUnits', 'auctionId', 'userIds']); + assert.isArray(auction.adUnits); + assert.isString(auction.auctionId); + assert.isArray(auction.userIds); + auction.adUnits.forEach(isValidAdUnit); + } + function isValidAdUnit(adUnit) { + assert.hasAllKeys(adUnit, ['transactionId', 'adUnitCode', 'slotId', 'mediaTypes', 'sizes', 'bids']); + assert.isString(adUnit.transactionId); + assert.isString(adUnit.adUnitCode); + assert.isString(adUnit.slotId); + assert.isArray(adUnit.mediaTypes); + assert.isArray(adUnit.sizes); + assert.isArray(adUnit.bids); + adUnit.mediaTypes.forEach(isValidMediaType); + adUnit.sizes.forEach(isValidSizeString); + adUnit.bids.forEach(isValidBid); + } + function isValidBid(bid) { + assert.containsAllKeys(bid, ['bidder', 'bidId', 'source', 'status', 'hasWon']); + if ('bidResponse' in bid) { + isValidBidResponse(bid.bidResponse); + } + assert.isString(bid.bidder); + assert.isString(bid.bidId); + assert.isString(bid.source); + assert.oneOf(bid.status, ['pending', 'timeout', 'targetingSet', 'rendered', 'success', 'rejected', 'no-bid', 'error']); + assert.oneOf(bid.hasWon, [0, 1]); + } + function isValidBidResponse(bidResponse) { + assert.containsAllKeys(bidResponse, ['mediaType', 'size', 'cur', 'cpm', 'cpmFloor']); + if ('cpmOrig' in bidResponse) { + assert.isNumber(bidResponse.cpmOrig); + } + isValidMediaType(bidResponse.mediaType); + isValidSizeString(bidResponse.size); + assert.isString(bidResponse.cur); + assert.isNumber(bidResponse.cpm); + assert.isNumber(bidResponse.cpmFloor); + } + function isValidMediaType(mediaType) { + assert.oneOf(mediaType, ['banner', 'video', 'native']); + } + function isValidSizeString(size) { + assert.match(size, /[0-9]+x[0-9]+/); + } + + function calledOnceWithStringJsonEquivalent(sinonSpy, ...args) { + sinon.assert.calledOnce(sinonSpy); + args.forEach((arg, i) => { + const stubCallArgs = sinonSpy.firstCall.args[i] + + if (typeof arg === 'object') { + assert.deepEqual(JSON.parse(stubCallArgs), arg); + } else { + assert.strictEqual(stubCallArgs, arg); + } + }); + } + + sinon.assert.expose(assert, { prefix: '' }); + return { + ...assert, + calledOnceWithStringJsonEquivalent, + isValidAnalyticsReport, + isValidAuction, + isValidAdUnit, + isValidBid, + isValidBidResponse, + isValidMediaType, + isValidSizeString, + } +}; + +function createReportWithThreeBidWonEvents() { + return { + pid: 'test-pid', + src: 'pbjs', + analyticsVersion: '1.0.0', + pbjsVersion: '$prebid.version$', + auctions: [{ + adUnits: [{ + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + adUnitCode: adUnitCodes[0], + slotId: adUnitCodes[0], + mediaTypes: ['banner'], + sizes: ['300x250', '300x600'], + bids: [{ + bidder: 'bidder0', + bidId: '20661fc5fbb5d9b', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '300x250' + }, + hasWon: 1 + }] + }, { + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + adUnitCode: adUnitCodes[1], + slotId: adUnitCodes[1], + mediaTypes: ['banner'], + sizes: ['728x90', '970x250'], + bids: [{ + bidder: 'bidder0', + bidId: '21ad295f40dd7ab', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }, { + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + adUnitCode: adUnitCodes[2], + slotId: adUnitCodes[2], + mediaTypes: ['banner'], + sizes: ['300x250'], + bids: [{ + bidder: 'bidder0', + bidId: '22108ac7b778717', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }], + auctionId: 'auction-000', + userIds: ['33acrossId'] + }], + }; +} + +function getMockEvents() { + const auctionId = 'auction-000'; + const userId = { + '33acrossId': { + envelope: 'v1.0014', + }, + }; + + return { + gam: { + slotRenderEnded: [ + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[0], divId: adSlotElementIds[0] }), + isEmpty: true, + slotContentChanged: true, + size: null, + advertiserId: null, + campaignId: null, + creativeId: null, + creativeTemplateId: null, + labelIds: null, + lineItemId: null, + isBackfill: false, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[1], divId: adSlotElementIds[1] }), + isEmpty: false, + slotContentChanged: true, + size: [1, 1], + advertiserId: 12345, + campaignId: 400000001, + creativeId: 6789, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1011, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[2], divId: adSlotElementIds[2] }), + isEmpty: false, + slotContentChanged: true, + size: [728, 90], + advertiserId: 12346, + campaignId: 299999000, + creativeId: 6790, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1012, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + ], + }, + prebid: [{ + AUCTION_INIT: { + auctionId, + adUnits: [ + { + code: adUnitCodes[0], + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [300, 250], + [300, 600], + ], + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + ortb2Imp: { + ext: { + gpid: adUnitCodes[0], + }, + }, + }, + { + code: adUnitCodes[1], + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [970, 250], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [728, 90], + [970, 250], + ], + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + ortb2Imp: { + ext: { + gpid: adUnitCodes[1], + }, + }, + }, + { + code: adUnitCodes[2], + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: '33across', + userId, + }, + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [[300, 250]], + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + ortb2Imp: { + ext: { + gpid: adUnitCodes[2], + }, + }, + }, + ], + bidderRequests: [ + { + bids: [ + { userId }, + ], + } + ], + }, + BID_REQUESTED: [ + { + auctionId, + bids: [ + { + bidder: 'bidder0', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + bidId: '20661fc5fbb5d9b', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + bidId: '21ad295f40dd7ab', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + bidId: '22108ac7b778717', + src: 'client', + }, + ], + }], + BID_RESPONSE: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'targetingSet' + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'targetingSet', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'targetingSet', + }], + BID_WON: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'rendered', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + }], + BID_REJECTED: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 2 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + width: 300, + height: 250, + source: 'client', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + statusMessage: 'Bid available', + rejectionReason: 'Bid does not meet price floor', + }], + AUCTION_END: { + auctionId, + }, + }], + }; +} diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 4f6d7c4a6c5..cbc5b277e30 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -1,4 +1,4 @@ -import { thirthyThreeAcrossIdSubmodule } from 'modules/33acrossIdSystem.js'; +import { thirthyThreeAcrossIdSubmodule, storage } from 'modules/33acrossIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; @@ -50,60 +50,300 @@ describe('33acrossIdSystem', () => { expect(completeCallback.calledOnceWithExactly('foo')).to.be.true; }); - context('when GDPR applies', () => { - it('should call endpoint with \'gdpr=1\'', () => { + context('if the use of a first-party ID has been enabled', () => { + context('and the response includes a first-party ID', () => { + context('and the storage type is "cookie"', () => { + it('should store the provided first-party ID in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + storeFpid: true + }, + storage: { + type: 'cookie', + expires: 30 + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.true; + + setCookie.restore(); + cookiesAreEnabled.restore(); + }); + }); + + context('and the storage type is "html5"', () => { + it('should store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + storeFpid: true + }, + storage: { + type: 'html5' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.true; + + setDataInLocalStorage.restore(); + }); + }); + }); + + context('and the response lacks a first-party ID', () => { + it('should wipe any existing first-party ID from storage', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345', + storeFpid: true + }, + storage: { + type: 'html5' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo' // no 'fp' field + }, + expires: 1645667805067 + })); + + expect(removeDataFromLocalStorage.calledOnceWithExactly('33acrossIdFp')).to.be.true; + expect(setCookie.calledOnceWithExactly('33acrossIdFp', '', sinon.match.string, 'Lax')).to.be.true; + + removeDataFromLocalStorage.restore(); + setCookie.restore(); + cookiesAreEnabled.restore(); + }); + }); + }); + + context('if the use of a first-party ID has been disabled (default value)', () => { + context('and the response includes a first-party ID', () => { + it('should not store the provided first-party ID in a cookie', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + // no storeFpid param + }, + storage: { + type: 'cookie', + expires: 30 + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setCookie.calledOnceWithExactly('33acrossIdFp', 'bar', sinon.match.string, 'Lax')).to.be.false; + + setCookie.restore(); + cookiesAreEnabled.restore(); + }); + + it('should not store the provided first-party ID in local storage', () => { + const completeCallback = () => {}; + + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + // no storeFpid param + }, + storage: { + type: 'html5' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + const setDataInLocalStorage = sinon.stub(storage, 'setDataInLocalStorage'); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: 'foo', + fp: 'bar' + }, + expires: 1645667805067 + })); + + expect(setDataInLocalStorage.calledOnceWithExactly('33acrossIdFp', 'bar')).to.be.false; + + setDataInLocalStorage.restore(); + }); + }); + }); + + context('if the response lacks the 33across "envelope" ID', () => { + it('should wipe any existing "envelope" ID from storage', () => { const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' + }, + storage: { + type: 'html5' } - }, { - gdprApplies: true }); callback(completeCallback); const [request] = server.requests; - expect(request.url).to.contain('gdpr=1'); + const removeDataFromLocalStorage = sinon.stub(storage, 'removeDataFromLocalStorage'); + const setCookie = sinon.stub(storage, 'setCookie'); + const cookiesAreEnabled = sinon.stub(storage, 'cookiesAreEnabled').returns(true); + + request.respond(200, { + 'Content-Type': 'application/json' + }, JSON.stringify({ + succeeded: true, + data: { + envelope: '' // no 'envelope' field + }, + expires: 1645667805067 + })); + + expect(removeDataFromLocalStorage.calledWith('33acrossId')).to.be.true; + expect(setCookie.calledWith('33acrossId', '', sinon.match.string, 'Lax')).to.be.true; + + removeDataFromLocalStorage.restore(); + setCookie.restore(); + cookiesAreEnabled.restore(); }); }); - context('when GDPR doesn\'t apply', () => { - it('should call endpoint with \'gdpr=0\'', () => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + context('when GDPR applies', () => { + it('should log a warning and don\'t expect a call to the endpoint', () => { + const logWarnSpy = sinon.spy(utils, 'logWarn'); + + const result = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - gdprApplies: false + gdprApplies: true }); - callback(completeCallback); - - const [request] = server.requests; + expect(logWarnSpy.calledOnceWithExactly('33acrossId: Submodule cannot be used where GDPR applies')).to.be.true; + expect(result).to.be.undefined; - expect(request.url).to.contain('gdpr=0'); + logWarnSpy.restore(); }); }); - context('when the GDPR consent string is given', () => { - it('should call endpoint with the GDPR consent string', () => { + context('when GDPR doesn\'t apply', () => { + it('should call endpoint with \'gdpr=0\'', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - consentString: 'foo' + gdprApplies: false }); callback(completeCallback); const [request] = server.requests; - expect(request.url).to.contain('gdpr_consent=foo'); + expect(request.url).to.contain('gdpr=0'); + }); + + context('but the GDPR consent string is given', () => { + it('should call endpoint with the GDPR consent string', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }, { + gdprApplies: false, + consentString: 'foo' + }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gdpr_consent=foo'); + }); }); }); @@ -252,6 +492,75 @@ describe('33acrossIdSystem', () => { }); }); + context('when a first-party ID is present in local storage', () => { + it('should call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + storage: { + type: 'html5' + } + }); + + sinon.stub(storage, 'getDataFromLocalStorage') + .withArgs('33acrossIdFp') + .returns('33acrossIdFpValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('fp=33acrossIdFpValue'); + + storage.getDataFromLocalStorage.restore(); + }); + }); + + context('when a first-party ID is present in cookie storage', () => { + it('should call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + }, + storage: { + type: 'cookie' + } + }); + + sinon.stub(storage, 'getCookie') + .withArgs('33acrossIdFp') + .returns('33acrossIdFpValue'); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('fp=33acrossIdFpValue'); + + storage.getCookie.restore(); + }); + }); + + context('when a first-party ID is not present in storage', () => { + it('should not call endpoint with the first-party ID included', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).not.to.contain('fp='); + }); + }); + context('when the partner ID is not given', () => { it('should log an error', () => { const logErrorSpy = sinon.spy(utils, 'logError'); diff --git a/test/spec/modules/BTBidAdapter_spec.js b/test/spec/modules/BTBidAdapter_spec.js new file mode 100644 index 00000000000..e0306abb7f0 --- /dev/null +++ b/test/spec/modules/BTBidAdapter_spec.js @@ -0,0 +1,209 @@ +import { expect } from 'chai'; +import { spec } from 'modules/BTBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; +// load modules that register ORTB processors +import 'src/prebid.js'; +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/consentManagementGpp.js'; +import 'modules/enrichmentFpdModule.js'; +import 'modules/gdprEnforcement.js'; +import 'modules/gppControl_usnat.js'; +import 'modules/schain.js'; + +describe('BT Bid Adapter', () => { + const ENDPOINT_URL = 'https://pbs.btloader.com/openrtb2/auction'; + const validBidRequests = [ + { + bidId: '2e9f38ea93bb9e', + bidder: 'blockthrough', + adUnitCode: 'adunit-code', + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + bidderA: { + pubId: '11111', + }, + }, + bidderRequestId: 'test-bidder-request-id', + }, + ]; + const bidderRequest = { + bidderCode: 'blockthrough', + bidderRequestId: 'test-bidder-request-id', + bids: validBidRequests, + }; + + describe('isBidRequestValid', function () { + it('should validate bid request with valid params', () => { + const validBid = { + params: { + pubmatic: { + publisherId: 55555, + }, + }, + sizes: [[300, 250]], + bidId: '123', + adUnitCode: 'leaderboard', + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.be.true; + }); + + it('should not validate bid request with invalid params', () => { + const invalidBid = { + params: {}, + sizes: [[300, 250]], + bidId: '123', + adUnitCode: 'leaderboard', + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.be.false; + }); + }); + + describe('buildRequests', () => { + it('should build post request when ortb2 fields are present', () => { + const impExtParams = { + bidderA: { + pubId: '11111', + }, + }; + + const requests = spec.buildRequests(validBidRequests, bidderRequest); + + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.equal(ENDPOINT_URL); + expect(requests[0].data).to.exist; + expect(requests[0].data.ext.prebid.channel).to.deep.equal({ + name: 'pbjs', + version: '$prebid.version$', + }); + expect(requests[0].data.imp[0].ext).to.deep.equal(impExtParams); + }); + }); + + describe('interpretResponse', () => { + it('should return empty array if serverResponse is not defined', () => { + const bidRequest = spec.buildRequests(validBidRequests, bidderRequest); + const bids = spec.interpretResponse(undefined, bidRequest); + + expect(bids.length).to.equal(0); + }); + + it('should return bids array when serverResponse is defined and seatbid array is not empty', () => { + const bidResponse = { + body: { + id: 'bid-response', + cur: 'USD', + seatbid: [ + { + bid: [ + { + impid: '2e9f38ea93bb9e', + crid: 'creative-id', + cur: 'USD', + price: 2, + w: 300, + h: 250, + mtype: 1, + adomain: ['test.com'], + }, + ], + seat: 'test-seat', + }, + ], + }, + }; + + const expectedBids = [ + { + btBidderCode: 'test-seat', + cpm: 2, + creativeId: 'creative-id', + creative_id: 'creative-id', + currency: 'USD', + height: 250, + mediaType: 'banner', + meta: { + advertiserDomains: ['test.com'], + }, + netRevenue: true, + requestId: '2e9f38ea93bb9e', + ttl: 60, + width: 300, + }, + ]; + + const request = spec.buildRequests(validBidRequests, bidderRequest)[0]; + const bids = spec.interpretResponse(bidResponse, request); + + expect(bids).to.deep.equal(expectedBids); + }); + }); + + describe('getUserSyncs', () => { + const SYNC_URL = 'https://cdn.btloader.com/user_sync.html'; + + it('should return an empty array if no sync options are provided', () => { + const syncs = spec.getUserSyncs({}, [], null, null, null); + + expect(syncs).to.deep.equal([]); + }); + + it('should return an empty array if no server responses are provided', () => { + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [], + null, + null, + null + ); + + expect(syncs).to.deep.equal([]); + }); + + it('should pass consent parameters and bidder codes in sync URL if they are provided', () => { + const gdprConsent = { + gdprApplies: true, + consentString: 'GDPRConsentString123', + }; + const gppConsent = { + gppString: 'GPPString123', + applicableSections: ['sectionA'], + }; + const us_privacy = '1YNY'; + const expectedSyncUrl = new URL(SYNC_URL); + expectedSyncUrl.searchParams.set('bidders', 'pubmatic,ix'); + expectedSyncUrl.searchParams.set('gdpr', 1); + expectedSyncUrl.searchParams.set( + 'gdpr_consent', + gdprConsent.consentString + ); + expectedSyncUrl.searchParams.set('gpp', gppConsent.gppString); + expectedSyncUrl.searchParams.set('gpp_sid', 'sectionA'); + expectedSyncUrl.searchParams.set('us_privacy', us_privacy); + const syncs = spec.getUserSyncs( + { iframeEnabled: true }, + [ + { body: { ext: { responsetimemillis: { pubmatic: 123 } } } }, + { body: { ext: { responsetimemillis: { pubmatic: 123, ix: 123 } } } }, + ], + gdprConsent, + us_privacy, + gppConsent + ); + + expect(syncs).to.deep.equal([ + { type: 'iframe', url: expectedSyncUrl.href }, + ]); + }); + }); +}); diff --git a/test/spec/modules/a1MediaBidAdapter_spec.js b/test/spec/modules/a1MediaBidAdapter_spec.js new file mode 100644 index 00000000000..e1db2b9ad8d --- /dev/null +++ b/test/spec/modules/a1MediaBidAdapter_spec.js @@ -0,0 +1,248 @@ +import { spec } from 'modules/a1MediaBidAdapter.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; +import 'modules/currency.js'; +import 'modules/priceFloors.js'; +import { replaceAuctionPrice } from '../../../src/utils'; + +const ortbBlockParams = { + battr: [ 13 ], + bcat: ['IAB1-1'] +}; +const getBidderRequest = (isMulti = false) => { + return { + bidderCode: 'a1media', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: '104e8d2392bd6f', + bids: [ + { + bidder: 'a1media', + params: {}, + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + mediaTypes: { + banner: { + sizes: [ + [ 320, 100 ], + ] + }, + ...(isMulti && { + video: { + mimes: ['video/mp4'] + }, + native: { + title: { + required: true, + }} + }) + }, + ...(isMulti && { + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + } + ] + } + }), + adUnitCode: 'test-div', + transactionId: 'cab00498-028b-4061-8f9d-a8d66c8cb91d', + bidId: '2e9f38ea93bb9e', + bidderRequestId: '104e8d2392bd6f', + } + ], + } +}; +const getConvertedBidReq = () => { + return { + cur: [ + 'JPY' + ], + imp: [ + { + banner: { + format: [ + { + h: 100, + w: 320 + }, + ], + topframe: 0 + }, + bidfloor: 0, + bidfloorcur: 'JPY', + id: '2e9f38ea93bb9e' + } + ], + test: 0, + } +}; + +const getBidderResponse = () => { + return { + body: { + id: 'bid-response', + cur: 'JPY', + seatbid: [ + { + bid: [{ + impid: '2e9f38ea93bb9e', + crid: 'creative-id', + cur: 'JPY', + price: 9, + }] + } + ] + } + } +} +const bannerAdm = '
'; +const videoAdm = 'testvast1'; +const nativeAdm = '{"ver":"1.2","link":{"url":"test_url"},"assets":[{"id":1,"required":1,"title":{"text":"native_title"}}]}'; +const macroAdm = '
'; +const macroNurl = 'https://d11.contentsfeed.com/dsp/win/example.com/SITE/a1/${AUCTION_PRICE}'; +const interpretedNurl = `
`; + +describe('a1MediaBidAdapter', function() { + describe('isValidRequest', function() { + const bid = { + bidder: 'a1media', + }; + + it('should return true always', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function() { + let bidderRequest, convertedRequest; + beforeEach(function() { + bidderRequest = getBidderRequest(); + convertedRequest = getConvertedBidReq(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + + expect(bidRequest.method).equal('POST'); + expect(bidRequest.url).equal('https://d11.contentsfeed.com/dsp/breq/a1'); + expect(bidRequest.data).deep.equal(convertedRequest); + }); + it('should set ortb blocking using params', function() { + bidderRequest.bids[0].params = ortbBlockParams; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.bcat = ortbBlockParams.bcat; + convertedRequest.imp[0].banner.battr = ortbBlockParams.battr; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + + it('should set bidfloor when getFloor is available', function() { + bidderRequest.bids[0].getFloor = () => ({ currency: 'USD', floor: 999 }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.imp[0].bidfloor).equal(999); + expect(bidRequest.data.imp[0].bidfloorcur).equal('USD'); + }); + + it('should set cur when currency config is configured', function() { + config.setConfig({ + currency: { + adServerCurrency: 'USD', + } + }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.cur[0]).equal('USD'); + }); + + it('should set bidfloor and currency using params when modules not available', function() { + bidderRequest.bids[0].params.currency = 'USD'; + bidderRequest.bids[0].params.bidfloor = 0.99; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.imp[0].bidfloor = 0.99; + convertedRequest.imp[0].bidfloorcur = 'USD'; + convertedRequest.cur[0] = 'USD'; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + }); + + describe('interpretResponse', function() { + describe('when request mediaType is single', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set cpm using price attribute', function() { + const bidResPrice = 9; + bidderResponse.body.seatbid[0].bid[0].price = bidResPrice; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].cpm).equal(bidResPrice); + }); + it('should set mediaType using request mediaTypes', function() { + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('when request mediaType is multi', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set mediaType to video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = videoAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(VIDEO); + }); + it('should set mediaType to native', function() { + bidderResponse.body.seatbid[0].bid[0].adm = nativeAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(NATIVE); + }); + it('should set mediaType to banner when adm is neither native or video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = bannerAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('resolve the AUCTION_PRICE macro', function() { + let bidRequest; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + }); + it('should return empty array when bid response has not contents', function() { + const emptyResponse = { body: '' }; + const interpretedRes = spec.interpretResponse(emptyResponse, bidRequest); + expect(interpretedRes.length).equal(0); + }); + it('should replace macro keyword if is exist', function() { + const bidderResponse = getBidderResponse(); + bidderResponse.body.seatbid[0].bid[0].adm = macroAdm; + bidderResponse.body.seatbid[0].bid[0].nurl = macroNurl; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + + const expectedResPrice = 9; + const expectedAd = replaceAuctionPrice(macroAdm, expectedResPrice) + replaceAuctionPrice(interpretedNurl, expectedResPrice); + + expect(interpretedRes[0].ad).equal(expectedAd); + }); + }); + }); +}) diff --git a/test/spec/modules/a1MediaRtdProvider_spec.js b/test/spec/modules/a1MediaRtdProvider_spec.js new file mode 100644 index 00000000000..2630e83fcf5 --- /dev/null +++ b/test/spec/modules/a1MediaRtdProvider_spec.js @@ -0,0 +1,105 @@ +import { subModuleObj } from 'modules/a1MediaRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { A1_AUD_KEY, A1_SEG_KEY, getStorageData, storage } from '../../../modules/a1MediaRtdProvider.js'; +import { expect } from 'chai'; + +const configWithParams = { + name: 'a1Media', + waitForIt: true, + params: { + tagId: 'lb4test.min.js', + }, +}; +const configWithoutParams = { + name: 'a1Media', + waitForIt: true, + params: { + }, +}; + +const reqBidsConfigObj = { + ortb2Fragments: { + global: {} + } +}; +const a1TestOrtbObj = { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900 + }, + segment: [{id: 'test'}] + } + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { + id: 'tester', + atype: 1 + } + ] + } + ] + } + } +}; + +describe('a1MediaRtdProvider', function() { + describe('init', function() { + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(subModuleObj.init(configWithParams)).to.be.true; + expect(window.linkback.l).to.be.true; + expect(loadExternalScript.called).to.be.true; + expect(loadExternalScript.args[0][0]).to.deep.equal('https://linkback.contentsfeed.com/src/lb4test.min.js'); + }) + + it('successfully initialize but script is already exist', function() { + const linkback = { l: true }; + + expect(subModuleObj.init(configWithParams)).to.be.true; + expect(loadExternalScript.called).to.be.false; + }) + }); + + describe('initialize without expected params', function() { + afterEach(function() { + storage.setCookie(A1_SEG_KEY, '', 0); + }) + + it('successfully initialize when publisher side segment is exist in cookie', function() { + storage.setCookie(A1_SEG_KEY, 'test'); + expect(subModuleObj.init(configWithoutParams)).to.be.true; + expect(getStorageData(A1_SEG_KEY)).to.not.equal(''); + }) + it('fails initalize publisher sied segment is not exist', function() { + expect(subModuleObj.init(configWithoutParams)).to.be.false; + expect(getStorageData(A1_SEG_KEY)).to.equal(''); + }) + }) + }); + + describe('alterBidRequests', function() { + const callback = sinon.stub(); + + before(function() { + storage.setCookie(A1_SEG_KEY, 'test'); + storage.setDataInLocalStorage(A1_AUD_KEY, 'tester'); + }) + after(function() { + storage.setCookie(A1_SEG_KEY, '', 0); + storage.removeDataFromLocalStorage(A1_AUD_KEY); + }) + + it('alterBidRequests', function() { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(a1TestOrtbObj); + expect(callback.calledOnce).to.be.true; + }) + }); +}) diff --git a/test/spec/modules/acuityadsBidAdapter_spec.js b/test/spec/modules/acuityadsBidAdapter_spec.js new file mode 100644 index 00000000000..31ef9dd6466 --- /dev/null +++ b/test/spec/modules/acuityadsBidAdapter_spec.js @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/acuityadsBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'acuityads' + +describe('AcuityAdsBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500, + ortb2: {} + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, 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://prebid.admanmedia.com/pbjs'); + }); + + it('Returns general data 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', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + 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.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, 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 = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, 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; + }); + + describe('Returns data with gppConsent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/ad2ictionBidAdapter_spec.js b/test/spec/modules/ad2ictionBidAdapter_spec.js new file mode 100644 index 00000000000..99800c6dd01 --- /dev/null +++ b/test/spec/modules/ad2ictionBidAdapter_spec.js @@ -0,0 +1,223 @@ +import { expect } from 'chai'; +import { + spec, + API_ENDPOINT, + API_VERSION_NUMBER, +} from 'modules/ad2ictionBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('ad2ictionBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + }, + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '2a7a3b48778a1b', + bidderRequestId: '1e6509293abe6b', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params id is not valid (letters)', function () { + const mockBid = { + ...bid, + params: { id: 1234 }, + }; + + expect(spec.isBidRequestValid(mockBid)).to.equal(false); + }); + + it('should return false when params id is not exist', function () { + const mockBid = { + ...bid, + }; + delete mockBid.params.id; + + expect(spec.isBidRequestValid(mockBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const mockValidBidRequests = [ + { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '57ffc0667379e1', + bidderRequestId: '4ddea14478a651', + }, + ]; + + const mockBidderRequest = { + bidderCode: 'ad2iction', + bidderRequestId: '4ddea14478a651', + bids: [ + { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + adUnitCode: 'adunit-code', + transactionId: null, + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '57ffc0667379e1', + bidderRequestId: '4ddea14478a651', + }, + ], + timeout: 1200, + refererInfo: { + ref: 'https://example.com/referer.html', + }, + ortb2: { + source: {}, + site: { + ref: 'https://example.com/referer.html', + }, + device: { + w: 390, + h: 844, + language: 'zh', + }, + }, + start: 1702526505498, + }; + + it('should send bid request to API_ENDPOINT via POST', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.url).to.equal(API_ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send bid request with API version', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.data.v).to.equal(API_VERSION_NUMBER); + }); + + it('should send bid request with dada fields', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.data).to.include.all.keys('udid', '_'); + expect(request.data).to.have.property('refererInfo'); + expect(request.data).to.have.property('ortb2'); + }); + }); + + describe('interpretResponse', function () { + it('should return an empty array to indicate no valid bids', function () { + const mockServerResponse = {}; + + const bidResponses = spec.interpretResponse(mockServerResponse); + + expect(bidResponses).is.an('array').that.is.empty; + }); + + it('should return a valid bid response', function () { + const MOCK_AD_DOM = "
" + const mockServerResponse = { + body: [ + { + requestId: '23a3d87fb6bde9', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + { + requestId: '3ce3efc40c890b', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + ], + }; + + const exceptServerResponse = [ + { + requestId: '23a3d87fb6bde9', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + { + requestId: '3ce3efc40c890b', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + ] + + const bidResponses = spec.interpretResponse(mockServerResponse); + + expect(bidResponses).to.eql(exceptServerResponse); + }); + }); +}); diff --git a/test/spec/modules/adagioAnalyticsAdapter_spec.js b/test/spec/modules/adagioAnalyticsAdapter_spec.js index 581f3cb1b87..64740f32b06 100644 --- a/test/spec/modules/adagioAnalyticsAdapter_spec.js +++ b/test/spec/modules/adagioAnalyticsAdapter_spec.js @@ -1,12 +1,14 @@ import adagioAnalyticsAdapter from 'modules/adagioAnalyticsAdapter.js'; import { expect } from 'chai'; import * as utils from 'src/utils.js'; +import { server } from 'test/mocks/xhr.js'; +import * as prebidGlobal from 'src/prebidGlobal.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); let constants = require('src/constants.json'); -describe('adagio analytics adapter', () => { +describe('adagio analytics adapter - adagio.js', () => { let sandbox; let adagioQueuePushSpy; @@ -174,3 +176,682 @@ describe('adagio analytics adapter', () => { }); }); }); + +const AUCTION_ID = '25c6d7f5-699a-4bfc-87c9-996f915341fa'; +const AUCTION_ID_ADAGIO = '6fc53663-bde5-427b-ab63-baa9ed296f47' +const AUCTION_ID_CACHE = 'b43d24a0-13d4-406d-8176-3181402bafc4'; +const AUCTION_ID_CACHE_ADAGIO = 'a9cae98f-efb5-477e-9259-27350044f8db'; + +const BID_ADAGIO = Object.assign({}, BID_ADAGIO, { + bidder: 'adagio', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.42, + currency: 'USD', + originalCpm: 1.42, + originalCurrency: 'USD', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + }, + pba: { + sid: '42', + e_pba_test: true + } +}); + +const BID_ANOTHER = Object.assign({}, BID_ANOTHER, { + bidder: 'another', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.71, + currency: 'EUR', + originalCpm: 1.62, + originalCurrency: 'GBP', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + } +}); + +const BID_CACHED = Object.assign({}, BID_ADAGIO, { + auctionId: AUCTION_ID_CACHE, + latestTargetedAuctionId: BID_ADAGIO.auctionId, +}); + +const PARAMS_ADG = { + organizationId: '1001', + site: 'test-com', + pageviewId: 'a68e6d70-213b-496c-be0a-c468ff387106', + environment: 'desktop', + pagetype: 'article', + placement: 'pave_top', + testName: 'test', + testVersion: 'version', +}; + +const AUCTION_INIT_ANOTHER = { + 'auctionId': AUCTION_ID, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'nobid', + 'params': { + 'publisherId': '1002' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'nobid', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_INIT_CACHE = { + 'auctionId': AUCTION_ID_CACHE, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_CACHE_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_END_ANOTHER = Object.assign({}, AUCTION_INIT_ANOTHER, { + bidsReceived: [BID_ANOTHER, BID_ADAGIO] +}); + +const AUCTION_END_ANOTHER_NOBID = Object.assign({}, AUCTION_INIT_ANOTHER, { + bidsReceived: [] +}); + +const MOCK = { + SET_TARGETING: { + [BID_ADAGIO.adUnitCode]: BID_ADAGIO.adserverTargeting, + [BID_ANOTHER.adUnitCode]: BID_ANOTHER.adserverTargeting + }, + AUCTION_INIT: { + another: AUCTION_INIT_ANOTHER, + bidcached: AUCTION_INIT_CACHE + }, + BID_RESPONSE: { + adagio: BID_ADAGIO, + another: BID_ANOTHER + }, + AUCTION_END: { + another: AUCTION_END_ANOTHER, + another_nobid: AUCTION_END_ANOTHER_NOBID + }, + BID_WON: { + adagio: Object.assign({}, BID_ADAGIO, { + 'status': 'rendered' + }), + another: Object.assign({}, BID_ANOTHER, { + 'status': 'rendered' + }), + bidcached: Object.assign({}, BID_CACHED, { + 'status': 'rendered' + }), + }, + AD_RENDER_SUCCEEDED: { + another: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_ANOTHER + }, + bidcached: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + }, + AD_RENDER_FAILED: { + bidcached: { + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + } +}; + +describe('adagio analytics adapter', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.registerAnalyticsAdapter({ + code: 'adagio', + adapter: adagioAnalyticsAdapter + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('track', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'adagio' + }); + }); + + afterEach(() => { + adagioAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', () => { + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + convertCurrency: (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + } + }); + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.another); + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); + + expect(server.requests.length).to.equal(3, 'requests count'); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,nobid'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('1,1,0'); + expect(search.bdrs_cpm).to.equal('1.42,2.052,'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('another'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_net_cpm).to.equal('2.052'); + expect(search.win_og_cpm).to.equal('2.592'); + } + }); + + it('builds and sends auction data with a cached bid win', () => { + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + convertCurrency: (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + } + }); + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.bidcached); + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another_nobid); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.bidcached); + events.emit(constants.EVENTS.AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED.bidcached); + + expect(server.requests.length).to.equal(5, 'requests count'); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another'); + expect(search.adg_mts).to.equal('ban'); + expect(search.t_n).to.equal('test'); + expect(search.t_v).to.equal('version'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,nobid'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('0,0,0'); + expect(search.bdrs_cpm).to.equal(',,'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('adagio'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_net_cpm).to.equal('1.42'); + expect(search.win_og_cpm).to.equal('1.42'); + expect(search.rndr).to.not.exist; + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[4].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('4'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.rndr).to.equal('0'); + } + }); + + it('send an "empty" cpm when adserver currency != USD and convertCurrency() is undefined', () => { + sandbox.stub(prebidGlobal, 'getGlobal').returns({}); + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.another); + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); + + expect(server.requests.length).to.equal(3, 'requests count'); + + // fail to compute bidder cpm and send an "empty" cpm + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('1,1,0'); + expect(search.bdrs_cpm).to.equal('1.42,,'); + } + }); + }); +}); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index f9bf62206f5..13c02cc9bae 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -322,6 +322,21 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].transactionId).to.not.exist; }); + it('should enrich prebid bid requests params', function() { + const expectedAuctionId = '373bcda7-9794-4f1c-be2c-0d223d11d579' + const expectedPageviewId = '56befc26-8cf0-472d-b105-73896df8eb89'; + sandbox.stub(utils, 'generateUUID').returns(expectedAuctionId); + sandbox.stub(adagio, 'getPageviewId').returns(expectedPageviewId); + + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + + spec.buildRequests([bid01], bidderRequest); + + expect(bid01.params.adagioAuctionId).eq(expectedAuctionId); + expect(bid01.params.pageviewId).eq(expectedPageviewId); + }); + it('should enqueue computed features for collect usage', function() { sandbox.stub(Date, 'now').returns(12345); @@ -697,6 +712,95 @@ describe('Adagio bid adapter', () => { }); }); + describe('with GPP', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + const regsGpp = 'regs_gpp_consent_string'; + const regsApplicableSections = [2]; + + const ortb2Gpp = 'ortb2_gpp_consent_string'; + const ortb2GppSid = [1]; + + context('When GPP in regs module', function() { + it('send gpp and gppSid to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + applicableSections: regsApplicableSections, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(regsGpp); + expect(requests[0].data.regs.gppSid).to.equal(regsApplicableSections); + }); + }); + + context('When GPP partially defined in regs module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + }, + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + + it('send empty gpp and gppSid if no ortb2 fields to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + + context('When GPP defined in ortb2 module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + }); + + context('When GPP not defined in any modules', function() { + it('send empty gpp and gppSid', function() { + const bidderRequest = new BidderRequestBuilder({}).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + }); + describe('with userID modules', function() { const userIdAsEids = [{ 'source': 'pubcid.org', @@ -757,11 +861,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(3); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({f: 1, mt: 'banner', s: '300x250'}); - expect(requests[0].data.adUnits[0].floors[1]).to.deep.equal({f: 1, mt: 'banner', s: '300x600'}); - expect(requests[0].data.adUnits[0].floors[2]).to.deep.equal({f: 1, mt: 'video', s: '600x480'}); - expect(requests[0].data.adUnits[0].mediaTypes.banner.sizes.length).to.equal(2); expect(requests[0].data.adUnits[0].mediaTypes.banner.bannerSizes[0]).to.deep.equal({size: [300, 250], floor: 1}); expect(requests[0].data.adUnits[0].mediaTypes.banner.bannerSizes[1]).to.deep.equal({size: [300, 600], floor: 1}); @@ -786,10 +885,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(2); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({f: 1, mt: 'video'}); - expect(requests[0].data.adUnits[0].floors[1]).to.deep.equal({f: 1, mt: 'native'}); - expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.equal(1); expect(requests[0].data.adUnits[0].mediaTypes.native.floor).to.equal(1); }); @@ -809,8 +904,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(1); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({mt: 'video'}); expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.be.undefined; }); }); @@ -854,6 +947,69 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.usIfr).to.equal(false); }); }); + + describe('with GPID', function () { + const gpid = '/12345/my-gpt-tag-0'; + + it('should add preferred gpid to the request', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { + gpid: gpid + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); + }); + + it('should add backup gpid to the request', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { + data: { pbadslot: gpid } + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); + }); + }); + + describe('with DSA', function() { + it('should add DSA to the request', function() { + const dsaObject = { + dsarequired: 1, + pubrender: 1, + datatopub: 2, + transparency: [{ + domain: 'domain.com', + dsaparams: [1, 2] + }] + } + + const bid01 = new BidRequestBuilder().withParams().build(); + + const bidderRequest = new BidderRequestBuilder({ + ortb2: { + regs: { + ext: { + dsa: dsaObject + } + } + } + }).build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.regs.dsa).to.deep.equal(dsaObject); + }); + + it('should not add DSA to the request if not present', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.regs.dsa).to.be.undefined; + }); + }) }); describe('interpretResponse()', function() { @@ -1008,7 +1164,7 @@ describe('Adagio bid adapter', () => { utilsMock.verify(); }); - describe('Response with video outstream', () => { + describe('Response with video outstream', function() { const bidRequestWithOutstream = utils.deepClone(bidRequest); bidRequestWithOutstream.data.adUnits[0].mediaTypes.video = { context: 'outstream', @@ -1081,7 +1237,7 @@ describe('Adagio bid adapter', () => { }); }); - describe('Response with native add', () => { + describe('Response with native add', function() { const serverResponseWithNative = utils.deepClone(serverResponse) serverResponseWithNative.body.bids[0].mediaType = 'native'; serverResponseWithNative.body.bids[0].admNative = { @@ -1258,6 +1414,24 @@ describe('Adagio bid adapter', () => { expect(r[0].native.javascriptTrackers).to.equal(expected); }); }); + + describe('Response with DSA', function() { + const dsaResponseObj = { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': { + 'domain': 'dsp1domain.com', + 'params': [1, 2] + }, + 'adrender': 1 + }; + + const serverResponseWithDsa = utils.deepClone(serverResponse); + serverResponseWithDsa.body.bids[0].meta.dsa = dsaResponseObj; + + const bidResponse = spec.interpretResponse(serverResponseWithDsa, bidRequest)[0]; + expect(bidResponse.meta.dsa).to.to.deep.equals(dsaResponseObj); + }) }); describe('getUserSyncs()', function() { @@ -1376,8 +1550,8 @@ describe('Adagio bid adapter', () => { expect(result.user_timestamp).to.be.a('String'); }); - it('should return `adunit_position` feature when the slot is hidden', function () { - const elem = fixtures.getElementById(); + it('should return `adunit_position` feature when the slot is hidden with value 0x0', function () { + const elem = fixtures.getElementById('0', '0', '0', '0'); sandbox.stub(window.top.document, 'getElementById').returns(elem); sandbox.stub(window.top, 'getComputedStyle').returns({ display: 'none' }); sandbox.stub(utils, 'inIframe').returns(false); @@ -1395,8 +1569,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bidRequest], bidderRequest); const result = requests[0].data.adUnits[0].features; - expect(result.adunit_position).to.match(/^[\d]+x[\d]+$/); - expect(elem.style.display).to.equal(null); // set null to reset style + expect(result.adunit_position).to.equal('0x0'); }); }); diff --git a/test/spec/modules/adbutlerBidAdapter_spec.js b/test/spec/modules/adbutlerBidAdapter_spec.js new file mode 100644 index 00000000000..6c38de717a3 --- /dev/null +++ b/test/spec/modules/adbutlerBidAdapter_spec.js @@ -0,0 +1,329 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adbutlerBidAdapter.js'; + +describe('AdButler adapter', function () { + let validBidRequests; + + beforeEach(function () { + validBidRequests = [ + { + bidder: 'adbutler', + params: { + accountID: '181556', + zoneID: '705374', + keyword: 'red', + minCPM: '1.00', + maxCPM: '5.00', + }, + placementCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }, + ]; + }); + + describe('for requests', function () { + describe('without account ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'adbutler', + params: { + zoneID: '210093', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('without a zone ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'adbutler', + params: { + accountID: '167283', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('with a valid bid', function () { + describe('with a custom domain', function () { + it('uses the custom domain', function () { + validBidRequests[0].params.domain = 'customadbutlerdomain.com'; + + const requests = spec.buildRequests(validBidRequests); + const requestURL = requests[0].url; + + expect(requestURL).to.have.string('customadbutlerdomain.com'); + }); + }); + + it('accepts the bid', function () { + const validBid = { + bidder: 'adbutler', + params: { + accountID: '167283', + zoneID: '210093', + }, + }; + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('sets default domain', function () { + const requests = spec.buildRequests(validBidRequests); + const request = requests[0]; + + let [domain] = request.url.split('/adserve/'); + + expect(domain).to.equal('https://servedbyadbutler.com'); + }); + + it('sets the keyword parameter', function () { + const requests = spec.buildRequests(validBidRequests); + const requestURL = requests[0].url; + + expect(requestURL).to.have.string(';kw=red;'); + }); + + describe('with extra params', function () { + beforeEach(function() { + validBidRequests[0].params.extra = { + foo: 'bar', + }; + }); + + it('sets the extra parameter', function () { + const requests = spec.buildRequests(validBidRequests); + const requestURL = requests[0].url; + + expect(requestURL).to.have.string(';foo=bar;'); + }); + }); + + describe('with multiple bids to the same zone', function () { + it('increments the place count', function () { + const requests = spec.buildRequests([validBidRequests[0], validBidRequests[0]]); + const firstRequest = requests[0].url; + const secondRequest = requests[1].url; + + expect(firstRequest).to.have.string(';place=0;'); + expect(secondRequest).to.have.string(';place=1;'); + }); + }); + }); + }); + + describe('for server responses', function () { + let serverResponse; + + describe('with no body', function () { + beforeEach(function() { + serverResponse = { + body: null, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with an incorrect size', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210083, + cpm: 1.5, + width: 728, + height: 90, + place: 0, + }, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with a failed status', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'NO_ELIGIBLE_ADS', + zone_id: 210083, + width: 300, + height: 250, + place: 0, + }, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with low CPM', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 0.75, + width: 300, + height: 250, + place: 0, + ad_code: '', + tracking_pixels: [], + }, + } + }); + + describe('with a minimum CPM', function () { + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + expect(bids).to.be.length(0); + }); + }); + + describe('with no minimum CPM', function () { + beforeEach(function() { + delete validBidRequests[0].params.minCPM; + }); + + it('returns a bid', function() { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + }); + }); + }); + + describe('with high CPM', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 999, + width: 300, + height: 250, + place: 0, + ad_code: '', + tracking_pixels: [], + }, + } + }); + + describe('with a maximum CPM', function () { + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with no maximum CPM', function () { + beforeEach(function() { + delete validBidRequests[0].params.maxCPM; + }); + + it('returns a bid', function() { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + }); + }); + }); + + describe('with a valid ad', function () { + beforeEach(function() { + serverResponse = { + body: { + status: 'SUCCESS', + account_id: 167283, + zone_id: 210093, + cpm: 1.5, + width: 300, + height: 250, + place: 0, + ad_code: '', + tracking_pixels: [ + 'http://tracking.pixel.com/params=info', + ], + }, + }; + }); + + it('returns a complete bid', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + expect(bids[0].cpm).to.equal(1.5); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].ad).to.have.string('http://tracking.pixel.com/params=info'); + }); + + describe('for a bid request without banner media type', function () { + beforeEach(function() { + delete validBidRequests[0].mediaTypes.banner; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with advertiser meta', function () { + beforeEach(function() { + serverResponse.body.advertiser = { + id: 123, + name: 'Advertiser Name', + domain: 'advertiser.com', + }; + }); + + it('returns a bid including advertiser meta', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + expect(bids[0]).to.have.property('meta'); + expect(bids[0].meta.advertiserId).to.equal(123); + expect(bids[0].meta.advertiserName).to.equal('Advertiser Name'); + expect(bids[0].meta.advertiserDomains).to.contain('advertiser.com'); + }); + }); + }); + }); +}); diff --git a/test/spec/modules/adfBidAdapter_spec.js b/test/spec/modules/adfBidAdapter_spec.js index 88f595b6c00..d4c5f5c3c38 100644 --- a/test/spec/modules/adfBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -1,4 +1,5 @@ // jshint esversion: 6, es3: false, node: true +/* eslint-disable no-console */ import { assert } from 'chai'; import { spec } from 'modules/adfBidAdapter.js'; import { config } from 'src/config.js'; @@ -141,6 +142,49 @@ describe('Adf adapter', function () { assert.equal(request.user, undefined); assert.equal(request.regs, undefined); }); + + it('should transfer DSA info', function () { + let validBidRequests = [ { bidId: 'bidId', params: { siteId: 'siteId' } } ]; + + let request = JSON.parse( + spec.buildRequests(validBidRequests, { + refererInfo: { page: 'page' }, + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [ + { + domain: 'test.com', + dsaparams: [1, 2, 3] + } + ] + } + } + } + } + }).data + ); + + assert.deepEqual(request.regs, { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [ + { + domain: 'test.com', + dsaparams: [1, 2, 3] + } + ] + } + } + }); + }); }); it('should add test and is_debug to request, if test is set in parameters', function () { @@ -446,6 +490,52 @@ describe('Adf adapter', function () { }); }); + it('should add correct params to getFloor', function () { + let result; + let mediaTypes = { video: { + playerSize: [ 100, 200 ] + } }; + const expectedFloors = [ 1, 1.3, 0.5 ]; + config.setConfig({ currency: { adServerCurrency: 'DKK' } }); + let validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = { banner: { + sizes: [ [100, 200], [300, 400] ] + }}; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = { native: {} }; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = {}; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + function getBidWithFloorTest(floor) { + return { + params: { mid: 1 }, + mediaTypes: mediaTypes, + getFloor: (args) => { + result = args; + return { + currency: 'DKK', + floor + }; + } + }; + } + }); + function getBidWithFloor(floor) { return { params: { mid: 1 }, @@ -960,7 +1050,16 @@ describe('Adf adapter', function () { adomain: [ 'demo.com' ], ext: { prebid: { - type: 'native' + type: 'native', + }, + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 } } } @@ -1023,6 +1122,15 @@ describe('Adf adapter', function () { assert.deepEqual(bids[0].mediaType, 'native'); assert.deepEqual(bids[0].meta.mediaType, 'native'); assert.deepEqual(bids[0].meta.advertiserDomains, [ 'demo.com' ]); + assert.deepEqual(bids[0].meta.dsa, { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + }); assert.deepEqual(bids[0].dealId, 'deal-id'); }); it('should set correct native params', function () { @@ -1213,6 +1321,32 @@ describe('Adf adapter', function () { assert.equal(bids[0].meta.mediaType, 'video'); }); + it('should set vastUrl if nurl is present in response', function () { + let vastUrl = 'http://url.to/vast' + let serverResponse = { + body: { + seatbid: [{ + bid: [{ impid: '1', adm: '', nurl: vastUrl, ext: { prebid: { type: 'video' } } }] + }] + } + }; + let bidRequest = { + data: {}, + bids: [ + { + bidId: 'bidId1', + params: { mid: 1000 } + } + ] + }; + + bids = spec.interpretResponse(serverResponse, bidRequest); + assert.equal(bids.length, 1); + assert.equal(bids[0].vastUrl, vastUrl); + assert.equal(bids[0].mediaType, 'video'); + assert.equal(bids[0].meta.mediaType, 'video'); + }); + it('should add renderer for outstream bids', function () { let serverResponse = { body: { diff --git a/test/spec/modules/adfusionBidAdapter_spec.js b/test/spec/modules/adfusionBidAdapter_spec.js new file mode 100644 index 00000000000..82705b727b4 --- /dev/null +++ b/test/spec/modules/adfusionBidAdapter_spec.js @@ -0,0 +1,130 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adfusionBidAdapter'; +import 'modules/priceFloors.js'; +import 'modules/currency.js'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('adfusionBidAdapter', function () { + const adapter = newBidder(spec); + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }; + + it('should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params.accountID is missing', function () { + let localbid = Object.assign({}, bid); + delete localbid.params.accountId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests, bannerBidRequest, bidderRequest; + beforeEach(function () { + bidRequests = [ + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }, + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + }, + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2', + }, + ]; + bannerBidRequest = { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }; + bidderRequest = { refererInfo: {} }; + }); + + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], bidderRequest); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.an('array'); + expect(request[0].data).to.be.an('object'); + expect(request[0].method).to.equal('POST'); + expect(request[0].currency).to.not.equal('USD'); + expect(request[0].url).to.not.equal(''); + expect(request[0].url).to.not.equal(undefined); + expect(request[0].url).to.not.equal(null); + }); + + it('should add bid floor', function () { + let bidRequest = Object.assign({}, bannerBidRequest); + let payload = spec.buildRequests([bidRequest], bidderRequest)[0].data; + expect(payload.imp[0].bidfloorcur).to.not.exist; + + let getFloorResponse = { currency: 'USD', floor: 3 }; + bidRequest.getFloor = () => getFloorResponse; + payload = spec.buildRequests([bidRequest], bidderRequest)[0].data; + expect(payload.imp[0].bidfloor).to.equal(3); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); + }); + }); +}); diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js index de8463731e0..adfd38d22cc 100644 --- a/test/spec/modules/adgenerationBidAdapter_spec.js +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -184,12 +184,12 @@ describe('AdgenerationAdapter', function () { } }; const data = { - banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com`, - native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&tp=https%3A%2F%2Fexample.com`, - bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, - bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=%5Bobject%20Object%5D&gpid=%252F1111%252Fhomepage%2523300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%257B%2522ver%2522%253A%25221.0%2522%252C%2522complete%2522%253A1%252C%2522nodes%2522%253A%255B%257B%2522asi%2522%253A%2522indirectseller.com%2522%252C%2522sid%2522%253A%252200001%2522%252C%2522hp%2522%253A1%257D%255D%257D&imark=1&tp=https%3A%2F%2Fexample.com`, + banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&tp=https%3A%2F%2Fexample.com`, + bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, + bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=AgAAAAVacu1uAxgAxH%2BHJ8%2BnWlS2H4uVqr6i%2BHBDCNREHD8WKsio%2Fx7D8xXFuq1cJycUU86yXfTH9Xe%2F4C8KkH%2B7UCiU7uQxhyD7Qxnv251pEs6K8oK%2BBPLYR%2B8BLY%2FsJKesa%2FkoKwx1FHgUzIBum582tSy2Oo%2B7C6wYUaaV4QcLr%2F4LPA%3D&gpid=%2F1111%2Fhomepage%23300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22indirectseller.com%22%2C%22sid%22%3A%2200001%22%2C%22hp%22%3A1%7D%5D%7D&imark=1&tp=https%3A%2F%2Fexample.com`, }; it('sends bid request to ENDPOINT via GET', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; diff --git a/test/spec/modules/adkernelAdnBidAdapter_spec.js b/test/spec/modules/adkernelAdnBidAdapter_spec.js index ff7ed9f145d..cfee5693cf5 100644 --- a/test/spec/modules/adkernelAdnBidAdapter_spec.js +++ b/test/spec/modules/adkernelAdnBidAdapter_spec.js @@ -426,8 +426,7 @@ describe('AdkernelAdn adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(1); - expect(spec.aliases[0]).to.be.equal('engagesimply'); + expect(spec.aliases).to.be.an('array'); }); }); }); diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 45498d2734a..cdfc9795b85 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -15,11 +15,13 @@ describe('Adkernel adapter', function () { auctionId: 'auc-001', mediaTypes: { banner: { - sizes: [[300, 250], [300, 200]] + sizes: [[300, 250], [300, 200]], + pos: 1 } }, ortb2Imp: { - battr: [6, 7, 9] + battr: [6, 7, 9], + pos: 2 } }, bid2_zone2 = { bidder: 'adkernel', @@ -103,7 +105,11 @@ describe('Adkernel adapter', function () { video: { context: 'instream', playerSize: [[640, 480]], - api: [1, 2] + api: [1, 2], + placement: 1, + plcmt: 1, + skip: 1, + pos: 1 } }, adUnitCode: 'ad-unit-1' @@ -119,7 +125,8 @@ describe('Adkernel adapter', function () { bidId: 'Bid_01', bidderRequestId: 'req-001', auctionId: 'auc-001' - }, bid_native = { + }, + bid_native = { bidder: 'adkernel', params: {zoneId: 1, host: 'rtb.adkernel.com'}, mediaTypes: { @@ -165,6 +172,33 @@ describe('Adkernel adapter', function () { } } }, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { + id: 0, required: 1, title: {len: 80} + }, { + id: 1, required: 1, data: {type: 2}}, + { + id: 2, required: 1, data: {type: 10} + }, { + id: 3, required: 1, img: {type: 1, wmin: 50, hmin: 50} + }, { + id: 4, required: 1, img: {type: 3, w: 300, h: 200} + }, { + id: 5, required: 0, data: {type: 3} + }, { + id: 6, required: 0, data: {type: 6} + }, { + id: 7, required: 0, data: {type: 12} + }, { + id: 8, required: 0, data: {type: 1} + }, { + id: 9, required: 0, data: {type: 11} + } + ], + privacy: 1 + }, adUnitCode: 'ad-unit-1', transactionId: 'f82c64b8-c602-42a4-9791-4a268f6559ed', bidId: 'Bid_01', @@ -244,6 +278,31 @@ describe('Adkernel adapter', function () { }], bidid: 'pTuOlf5KHUo', cur: 'EUR' + }, + multiformat_response = { + id: '47ce4badcf7482', + seatbid: [{ + bid: [{ + id: 'sZSYq5zYMxo_0', + impid: 'Bid_01b__mf', + crid: '100_003', + price: 0.00145, + adid: '158801', + adm: '', + nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl', + cid: '16855' + }, { + id: 'sZSYq5zYMxo_1', + impid: 'Bid_01v__mf', + crid: '100_003', + price: 0.25, + adid: '158801', + nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_1&f=nurl', + cid: '16855' + }] + }], + bidid: 'pTuOlf5KHUo', + cur: 'USD' }; var sandbox; @@ -346,6 +405,11 @@ describe('Adkernel adapter', function () { expect(bidRequest.imp[0].banner.battr).to.be.eql([6, 7, 9]); }); + it('should respect mediatypes attributes over FPD', function() { + expect(bidRequest.imp[0].banner).to.have.property('pos'); + expect(bidRequest.imp[0].banner.pos).to.be.eql(1); + }); + it('shouldn\'t contain gdpr nor ccpa information for default request', function () { let [_, bidRequests] = buildRequest([bid1_zone1]); expect(bidRequests[0]).to.not.have.property('regs'); @@ -354,11 +418,16 @@ describe('Adkernel adapter', function () { it('should contain gdpr-related information if consent is configured', function () { let [_, bidRequests] = buildRequest([bid1_zone1], - buildBidderRequest('https://example.com/index.html', - {gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}, uspConsent: '1YNN'})); + buildBidderRequest('https://example.com/index.html', { + gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}, + uspConsent: '1YNN', + gppConsent: {gppString: 'DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', applicableSections: [2]}} + )); let bidRequest = bidRequests[0]; expect(bidRequest).to.have.property('regs'); expect(bidRequest.regs.ext).to.be.eql({'gdpr': 1, 'us_privacy': '1YNN'}); + expect(bidRequest.regs.gpp).to.be.eql('DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(bidRequest.regs.gpp_sid).to.be.eql([2]); expect(bidRequest).to.have.property('user'); expect(bidRequest.user.ext).to.be.eql({'consent': 'test-consent-string'}); }); @@ -433,24 +502,40 @@ describe('Adkernel adapter', function () { }); it('should have openrtb video impression parameters', function() { - expect(bidRequests[0].imp[0].video).to.have.property('api'); - expect(bidRequests[0].imp[0].video.api).to.be.eql([1, 2]); + let video = bidRequests[0].imp[0].video; + expect(video).to.have.property('api'); + expect(video.api).to.be.eql([1, 2]); + expect(video.placement).to.be.eql(1); + expect(video.plcmt).to.be.eql(1); + expect(video.skip).to.be.eql(1); + expect(video.pos).to.be.eql(1); }); }); describe('multiformat request building', function () { - let _, bidRequests; + let pbRequests, bidRequests; before(function () { - [_, bidRequests] = buildRequest([bid_multiformat]); + [pbRequests, bidRequests] = buildRequest([bid_multiformat]); }); it('should contain single request', function () { expect(bidRequests).to.have.length(1); - expect(bidRequests[0].imp).to.have.length(1); }); - it('should contain banner-only impression', function () { - expect(bidRequests[0].imp).to.have.length(1); + it('should contain both impression', function () { + expect(bidRequests[0].imp).to.have.length(2); expect(bidRequests[0].imp[0]).to.have.property('banner'); - expect(bidRequests[0].imp[0]).to.not.have.property('video'); + expect(bidRequests[0].imp[1]).to.have.property('video'); + // check that splitted imps do not share same impid + expect(bidRequests[0].imp[0].id).to.be.not.eql('Bid_01'); + expect(bidRequests[0].imp[1].id).to.be.not.eql('Bid_01'); + expect(bidRequests[0].imp[1].id).to.be.not.eql(bidRequests[0].imp[0].id); + }); + it('x', function() { + let bids = spec.interpretResponse({body: multiformat_response}, pbRequests[0]); + expect(bids).to.have.length(2); + expect(bids[0].requestId).to.be.eql('Bid_01'); + expect(bids[0].mediaType).to.be.eql('banner'); + expect(bids[1].requestId).to.be.eql('Bid_01'); + expect(bids[1].mediaType).to.be.eql('video'); }); }); @@ -622,18 +707,18 @@ describe('Adkernel adapter', function () { expect(bidRequests[0].imp[0]).to.have.property('native'); expect(bidRequests[0].imp[0].native).to.have.property('request'); let request = JSON.parse(bidRequests[0].imp[0].native.request); - expect(request).to.have.property('ver', '1.1'); + expect(request).to.have.property('ver', '1.2'); expect(request.assets).to.have.length(10); expect(request.assets[0]).to.be.eql({id: 0, required: 1, title: {len: 80}}); - expect(request.assets[1]).to.be.eql({id: 3, required: 1, data: {type: 2}}); - expect(request.assets[2]).to.be.eql({id: 4, required: 1, data: {type: 10}}); - expect(request.assets[3]).to.be.eql({id: 1, required: 1, img: {wmin: 50, hmin: 50, type: 1}}); - expect(request.assets[4]).to.be.eql({id: 2, required: 1, img: {w: 300, h: 200, type: 3}}); - expect(request.assets[5]).to.be.eql({id: 11, required: 0, data: {type: 3}}); - expect(request.assets[6]).to.be.eql({id: 8, required: 0, data: {type: 6}}); - expect(request.assets[7]).to.be.eql({id: 10, required: 0, data: {type: 12}}); - expect(request.assets[8]).to.be.eql({id: 5, required: 0, data: {type: 1}}); - expect(request.assets[9]).to.be.eql({id: 14, required: 0, data: {type: 11}}); + expect(request.assets[1]).to.be.eql({id: 1, required: 1, data: {type: 2}}); + expect(request.assets[2]).to.be.eql({id: 2, required: 1, data: {type: 10}}); + expect(request.assets[3]).to.be.eql({id: 3, required: 1, img: {wmin: 50, hmin: 50, type: 1}}); + expect(request.assets[4]).to.be.eql({id: 4, required: 1, img: {w: 300, h: 200, type: 3}}); + expect(request.assets[5]).to.be.eql({id: 5, required: 0, data: {type: 3}}); + expect(request.assets[6]).to.be.eql({id: 6, required: 0, data: {type: 6}}); + expect(request.assets[7]).to.be.eql({id: 7, required: 0, data: {type: 12}}); + expect(request.assets[8]).to.be.eql({id: 8, required: 0, data: {type: 1}}); + expect(request.assets[9]).to.be.eql({id: 9, required: 0, data: {type: 11}}); }); it('native response processing', () => { @@ -650,15 +735,21 @@ describe('Adkernel adapter', function () { 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'); - expect(resp.native.impressionTrackers).to.be.eql(['http://rtb.com/win?i=pTuOlf5KHUo_0&f=imp']); - expect(resp.native).to.have.property('title', 'Title'); - expect(resp.native).to.have.property('body', 'Description'); - expect(resp.native).to.have.property('body2', 'Additional description'); - expect(resp.native.icon).to.be.eql({url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0&imgt=icon', width: 50, height: 50}); - expect(resp.native.image).to.be.eql({url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0', width: 300, height: 200}); - expect(resp.native).to.have.property('sponsoredBy', 'Sponsor.com'); - expect(resp.native).to.have.property('displayUrl', 'displayurl.com'); + expect(resp.native).to.have.property('ortb'); + + expect(resp.native.ortb).to.be.eql({ + assets: [ + {id: 0, title: {text: 'Title'}}, + {id: 3, data: {value: 'Description'}}, + {id: 4, data: {value: 'Additional description'}}, + {id: 1, img: {url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0&imgt=icon', w: 50, h: 50}}, + {id: 2, img: {url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0', w: 300, h: 200}}, + {id: 5, data: {value: 'Sponsor.com'}}, + {id: 14, data: {value: 'displayurl.com'}} + ], + link: {url: 'http://rtb.com/click?i=pTuOlf5KHUo_0'}, + imptrackers: ['http://rtb.com/win?i=pTuOlf5KHUo_0&f=imp'] + }); }); }); }); diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index a071c6bbe3f..58277bc830d 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -1,11 +1,11 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; -import { ajax } from 'src/ajax.js'; import { buildVideoUrl } from 'modules/adlooxAdServerVideo.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import { targeting } from 'src/targeting.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -199,11 +199,9 @@ describe('Adloox Ad Server Video', function () { }); describe('process VAST', function () { - let server = null; let BID = null; let getWinningBidsStub; beforeEach(function () { - server = sinon.createFakeServer(); BID = utils.deepClone(bid); getWinningBidsStub = sinon.stub(targeting, 'getWinningBids') getWinningBidsStub.withArgs(adUnit.code).returns([ BID ]); @@ -212,8 +210,6 @@ describe('Adloox Ad Server Video', function () { getWinningBidsStub.restore(); getWinningBidsStub = undefined; BID = null; - server.restore(); - server = null; }); it('should return URL unchanged for non-VAST', function (done) { diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 5b99789981f..0e26ef1afdb 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,12 +1,12 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; import {auctionManager} from 'src/auctionManager.js'; -import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { subModuleObj as rtdProvider } from 'modules/adlooxRtdProvider.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -139,16 +139,12 @@ describe('Adloox RTD Provider', function () { expect(analyticsAdapter.context).is.null; }); - let server = null; let CONFIG = null; beforeEach(function () { - server = sinon.createFakeServer(); CONFIG = utils.deepClone(config); }); afterEach(function () { CONFIG = null; - server.restore(); - server = null; }); it('should fetch segments', function (done) { diff --git a/test/spec/modules/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index 1d2fb1e79cb..8730f2c1a0d 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -1,12 +1,553 @@ -import {expect} from 'chai'; -import {spec, storage} from 'modules/admaticBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; +import { expect } from 'chai'; +import { spec } from 'modules/admaticBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; const ENDPOINT = 'https://layer.serve.admatic.com.tr/pb'; describe('admaticBidAdapter', () => { const adapter = newBidder(spec); + let validRequest = [ { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, + 'bidder': 'admatic', + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [728, 90]] + }, + 'native': { + }, + 'video': { + } + }, + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'pixad.com.tr', + 'sid': 'px-pub-3000856707', + 'hp': 1 + } + ] + }, + 'at': 1, + 'tmax': 1000, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ] + } + }, + 'ortb': { + 'badv': [], + 'bcat': [], + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' + } + }, + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost', + 'publisherId': 12321312 + } + }, + 'imp': [ + { + 'size': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 728, + 'h': 90 + } + ], + 'mediatype': {}, + 'type': 'banner', + 'id': 1, + 'floors': { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + } + }, + { + 'size': [ + { + 'w': 338, + 'h': 280 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 338, + 280 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2 + }, + 'floors': { + 'video': { + '338x280': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'floors': { + 'native': { + '*': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '16e0c8982318f91' + } + ], + 'ext': { + 'cur': 'USD', + 'bidder': 'admatic' + } + } ]; + let bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, + 'bidder': 'admatic', + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [728, 90]] + }, + 'native': { + }, + 'video': { + 'playerSize': [ + 336, + 280 + ] + } + }, + 'userId': { + 'id5id': { + 'uid': '0', + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + }, + 'pubcid': '5a49273f-a424-454b-b478-169c3551aa72' + }, + 'userIdAsEids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ], + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'pixad.com.tr', + 'sid': 'px-pub-3000856707', + 'hp': 1 + } + ] + }, + 'at': 1, + 'tmax': 1000, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ] + } + }, + 'ortb': { + 'source': {}, + 'site': { + 'domain': 'localhost:8888', + 'publisher': { + 'domain': 'localhost:8888' + }, + 'page': 'http://localhost:8888/', + 'name': 'http://localhost:8888' + }, + 'badv': [], + 'bcat': [], + 'device': { + 'w': 896, + 'h': 979, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '119' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '119' + ] + }, + { + 'brand': 'Not?A_Brand', + 'version': [ + '24' + ] + } + ], + 'mobile': 0 + } + } + }, + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost', + 'publisherId': 12321312 + } + }, + 'imp': [ + { + 'size': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 728, + 'h': 90 + } + ], + 'id': 1, + 'mediatype': {}, + 'type': 'banner', + 'floors': { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + } + }, + { + 'size': [ + { + 'w': 338, + 'h': 280 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 338, + 280 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2 + }, + 'floors': { + 'video': { + '338x280': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'floors': { + 'native': { + '*': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '16e0c8982318f91' + } + ], + 'ext': { + 'cur': 'USD', + 'bidder': 'admatic' + } + }; describe('inherited functions', () => { it('exists and is a function', () => { @@ -16,6 +557,10 @@ describe('admaticBidAdapter', () => { describe('isBidRequestValid', function() { let bid = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, @@ -28,6 +573,7 @@ describe('admaticBidAdapter', () => { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'creativeId': 'er2ee', + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] } }; @@ -46,252 +592,147 @@ describe('admaticBidAdapter', () => { describe('buildRequests', function () { it('sends bid request to ENDPOINT via POST', function () { - let validRequest = [ { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } - }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else { - return {} - } - }, - 'user': { - 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' - }, - 'blacklist': [], - 'site': { - 'page': 'http://localhost:8888/admatic.html', - 'ref': 'http://localhost:8888', - 'publisher': { - 'name': 'localhost', - 'publisherId': 12321312 + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should not populate GDPR if for non-EEA users', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } - }, - 'imp': [ - { - 'size': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 728, - 'h': 90 - } - ], - 'mediatype': {}, - 'type': 'banner', - 'id': '2205da7a81846b', - 'floors': { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - } - } - }, - { - 'size': [ - { - 'w': 338, - 'h': 280 - } - ], - 'type': 'video', - 'mediatype': { - 'context': 'instream', - 'mimes': [ - 'video/mp4' - ], - 'maxduration': 240, - 'api': [ - 1, - 2 - ], - 'playerSize': [ - [ - 338, - 280 - ] - ], - 'protocols': [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - 'skip': 1, - 'playbackmethod': [ - 2 - ], - 'linearity': 1, - 'placement': 2 - }, - 'id': '45e86fc7ce7fc93' + }) + ); + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.regs.ext.consent).to.equal('BOJ8RZsOJ8RZsABAB8AAAAAZ-A'); + }); + + it('should populate GDPR and empty consent string if available for EEA users without consent string but with consent', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true } - ], - 'ext': { - 'cur': 'USD', - 'bidder': 'admatic' + }) + ); + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.regs.ext.consent).to.equal(''); + }); + + it('should properly build a request when coppa flag is true', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + coppa: true + }) + ); + expect(request.data.regs.ext.coppa).to.not.be.undefined; + expect(request.data.regs.ext.coppa).to.equal(1); + }); + + it('should properly build a request with gpp consent field', function () { + let bidRequest = Object.assign([], validRequest); + const ortb2 = { + regs: { + gpp: 'gpp_consent_string', + gpp_sid: [0, 1, 2] } - } ]; - let bidderRequest = { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } - }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else { - return {} - } - }, - 'user': { - 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' - }, - 'blacklist': [], - 'site': { - 'page': 'http://localhost:8888/admatic.html', - 'ref': 'http://localhost:8888', - 'publisher': { - 'name': 'localhost', - 'publisherId': 12321312 - } - }, - 'imp': [ - { - 'size': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 728, - 'h': 90 - } - ], - 'id': '2205da7a81846b', - 'mediatype': {}, - 'type': 'banner', - 'floors': { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - } + }; + const request = spec.buildRequests(bidRequest, { ...bidderRequest, ortb2 }); + expect(request.data.regs.ext.gpp).to.equal('gpp_consent_string'); + expect(request.data.regs.ext.gpp_sid).to.deep.equal([0, 1, 2]); + }); + + it('should properly build a request with ccpa consent field', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + uspConsent: '1---' + }) + ); + expect(request.data.regs.ext.uspIab).to.not.be.null; + expect(request.data.regs.ext.uspIab).to.equal('1---'); + }); + + it('should properly forward eids', function () { + const bidRequests = [ + { + bidder: 'admatic', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] } }, - { - 'size': [ - { - 'w': 338, - 'h': 280 - } - ], - 'type': 'video', - 'mediatype': { - 'context': 'instream', - 'mimes': [ - 'video/mp4' - ], - 'maxduration': 240, - 'api': [ - 1, - 2 - ], - 'playerSize': [ - [ - 338, - 280 - ] - ], - 'protocols': [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - 'skip': 1, - 'playbackmethod': [ - 2 - ], - 'linearity': 1, - 'placement': 2 - }, - 'id': '45e86fc7ce7fc93' - } - ], - 'ext': { - 'cur': 'USD', - 'bidder': 'admatic' + userIdAsEids: [ + { + source: 'admatic.com.tr', + uids: [{ + id: 'abc', + atype: 1 + }] + } + ], + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.user.ext.eids).to.deep.equal([ + { + source: 'admatic.com.tr', + uids: [{ + id: 'abc', + atype: 1 + }] } - }; - const request = spec.buildRequests(validRequest, bidderRequest); - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('POST'); + ]); }); it('should properly build a banner request with floors', function () { - let bidRequests = [ + const request = spec.buildRequests(validRequest, bidderRequest); + request.data.imp[0].floors = { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }; + }); + + it('should properly build a video request with several player sizes with floors', function () { + const bidRequests = [ { 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, + 'adUnitCode': 'bid-123', + 'transactionId': 'transaction-123', 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] + 'video': { + 'playerSize': [[300, 250], [728, 90]] } }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + if (inputParams.mediaType === VIDEO && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', floor: 1.0 }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', floor: 2.0 @@ -302,32 +743,50 @@ describe('admaticBidAdapter', () => { } }, ]; - let bidderRequest = { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [728, 90]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'creativeId': 'er2ee', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } + const bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', } }; const request = spec.buildRequests(bidRequests, bidderRequest); - request.data.imp[0].floors = { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } + }); + + it('should properly build a native request with floors', function () { + const bidRequests = [ + { + 'bidder': 'admatic', + 'adUnitCode': 'bid-123', + 'transactionId': 'transaction-123', + 'mediaTypes': { + 'native': { + } + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + getFloor: inputParams => { + if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + } + }, + ]; + const bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', } }; + const request = spec.buildRequests(bidRequests, bidderRequest); }); }); @@ -343,6 +802,10 @@ describe('admaticBidAdapter', () => { 'price': 0.01, 'type': 'banner', 'bidder': 'admatic', + 'mime_type': { + 'name': 'backfill', + 'force': false + }, 'adomain': ['admatic.com.tr'], 'party_tag': '
', 'iurl': 'https://www.admatic.com.tr' @@ -354,6 +817,10 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', + 'mime_type': { + 'name': 'backfill', + 'force': false + }, 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': '', @@ -361,14 +828,18 @@ describe('admaticBidAdapter', () => { }, { 'id': 3, - 'creative_id': '3741', - 'width': 300, - 'height': 250, + 'creative_id': '3742', + 'width': 1, + 'height': 1, 'price': 0.01, - 'type': 'video', + 'type': 'native', + 'mime_type': { + 'name': 'backfill', + 'force': false + }, 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], - 'party_tag': 'https://www.admatic.com.tr', + 'party_tag': '{"native":{"ver":"1.1","assets":[{"id":1,"title":{"text":"title"}},{"id":4,"data":{"value":"body"}},{"id":5,"data":{"value":"sponsored"}},{"id":6,"data":{"value":"cta"}},{"id":2,"img":{"url":"https://www.admatic.com.tr","w":1200,"h":628}},{"id":3,"img":{"url":"https://www.admatic.com.tr","w":640,"h":480}}],"link":{"url":"https://www.admatic.com.tr"},"imptrackers":["https://www.admatic.com.tr"]}}', 'iurl': 'https://www.admatic.com.tr' } ], @@ -388,6 +859,10 @@ describe('admaticBidAdapter', () => { ad: '
', creativeId: '374', meta: { + model: { + 'name': 'backfill', + 'force': false + }, advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -401,10 +876,13 @@ describe('admaticBidAdapter', () => { currency: 'TRY', mediaType: 'video', netRevenue: true, - vastImpUrl: 'https://www.admatic.com.tr', vastXml: '', creativeId: '3741', meta: { + model: { + 'name': 'backfill', + 'force': false + }, advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -413,15 +891,35 @@ describe('admaticBidAdapter', () => { { requestId: 3, cpm: 0.01, - width: 300, - height: 250, + width: 1, + height: 1, currency: 'TRY', - mediaType: 'video', + mediaType: 'native', netRevenue: true, - vastImpUrl: 'https://www.admatic.com.tr', - vastXml: 'https://www.admatic.com.tr', - creativeId: '3741', + native: { + 'clickUrl': 'https://www.admatic.com.tr', + 'impressionTrackers': ['https://www.admatic.com.tr'], + 'title': 'title', + 'body': 'body', + 'sponsoredBy': 'sponsored', + 'cta': 'cta', + 'image': { + 'url': 'https://www.admatic.com.tr', + 'width': 1200, + 'height': 628 + }, + 'icon': { + 'url': 'https://www.admatic.com.tr', + 'width': 640, + 'height': 480 + } + }, + creativeId: '3742', meta: { + model: { + 'name': 'backfill', + 'force': false + }, advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -432,8 +930,206 @@ describe('admaticBidAdapter', () => { ext: { 'cur': 'TRY', 'type': 'admatic' - } + }, + imp: [ + { + 'size': [ + { + 'w': 320, + 'h': 100 + } + ], + 'type': 'banner', + 'mediatype': {}, + 'ext': { + 'instl': 0, + 'gpid': 'desktop-standard', + 'pxid': [ + '1111111111' + ], + 'pxtype': 'pixad', + 'ortbstatus': true, + 'viewability': 100, + 'data': { + 'pbadslot': 'desktop-standard' + }, + 'ae': 1 + }, + 'id': 1, + 'floors': { + 'banner': { + '320x100': { + 'floor': 0.1, + 'currency': 'TRY' + } + } + } + }, + { + 'size': [ + { + 'w': 320, + 'h': 100 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 320, + 100 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 0, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2, + 'plcmt': 4 + }, + 'ext': { + 'gpid': 'outstream-desktop-standard', + 'pxtype': 'pixad', + 'pxid': [ + '1111111111' + ], + 'ortbstatus': true, + 'viewability': 100, + 'data': { + 'pbadslot': 'outstream-desktop-standard' + }, + 'ae': 1 + }, + 'id': 2, + 'floors': { + 'video': { + '320x100': { + 'floor': 0.1, + 'currency': 'TRY' + } + } + } + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'sendTargetingKeys': false, + 'ortb': { + 'ver': '1.1', + 'context': 2, + 'plcmttype': 1, + 'privacy': 1, + 'assets': [ + { + 'id': 1, + 'required': 1, + 'title': { + 'len': 120 + } + }, + { + 'id': 2, + 'required': 1, + 'img': { + 'type': 3, + 'w': 640, + 'h': 480 + } + }, + { + 'id': 3, + 'required': 0, + 'img': { + 'type': 1, + 'w': 640, + 'h': 480 + } + }, + { + 'id': 4, + 'required': 0, + 'data': { + 'type': 2 + } + }, + { + 'id': 5, + 'required': 0, + 'data': { + 'type': 1 + } + }, + { + 'id': 6, + 'required': 0, + 'data': { + 'type': 11 + } + } + ], + 'eventtrackers': [ + { + 'event': 1, + 'methods': [ + 1, + 2 + ] + } + ] + } + }, + 'ext': { + 'gpid': 'native-desktop-standard', + 'pxtype': 'pixad', + 'pxid': [ + '1111111111' + ], + 'ortbstatus': true, + 'viewability': 100, + 'data': { + 'pbadslot': 'native-desktop-standard' + }, + 'ae': 1 + }, + 'id': 3, + 'floors': { + 'native': { + '*': { + 'floor': 0.1, + 'currency': 'TRY' + } + } + } + } + ] }; + let result = spec.interpretResponse(bids, {data: request}); expect(result).to.eql(expectedResponse); }); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 8cf433460b7..85538efc957 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -4,11 +4,12 @@ import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config.js'; const BIDDER_CODE = 'admixer'; -const BIDDER_CODE_ADX = 'admixeradx'; +const WL_BIDDER_CODE = 'admixerwl' const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; const ENDPOINT_URL_CUSTOM = 'https://custom.admixer.net/prebid.aspx'; -const ENDPOINT_URL_ADX = 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; +const CLIENT_ID = 5124; +const ENDPOINT_ID = 81264; describe('AdmixerAdapter', function () { const adapter = newBidder(spec); @@ -36,9 +37,28 @@ describe('AdmixerAdapter', function () { auctionId: '1d1a030790a475', }; + let wlBid = { + bidder: WL_BIDDER_CODE, + params: { + clientId: CLIENT_ID, + endpointId: ENDPOINT_ID, + }, + 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 true when params required by WL found', function () { + expect(spec.isBidRequestValid(wlBid)).to.equal(true); + }); it('should return false when required params are not passed', function () { let bid = Object.assign({}, bid); @@ -48,6 +68,14 @@ describe('AdmixerAdapter', function () { }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when params required by WL are not passed', function () { + let wlBid = Object.assign({}, wlBid); + delete wlBid.params; + wlBid.params = { + clientId: 0, + }; + expect(spec.isBidRequestValid(wlBid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -105,7 +133,10 @@ describe('AdmixerAdapter', function () { validRequest: [ { bidder: bidder, - params: { + params: bidder === 'admixerwl' ? { + clientId: CLIENT_ID, + endpointId: ENDPOINT_ID + } : { zone: ZONE_ID, }, adUnitCode: 'adunit-code', @@ -168,6 +199,12 @@ describe('AdmixerAdapter', function () { expect(request.url).to.equal('https://inv-nets.admixer.net/adxprebid.1.2.aspx'); expect(request.method).to.equal('POST'); }); + it('build request for admixerwl', function () { + const requestParams = requestParamsFor('admixerwl'); + const request = spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest); + expect(request.url).to.equal(`https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx?client=${CLIENT_ID}`); + expect(request.method).to.equal('POST'); + }); }); describe('checkFloorGetting', function () { diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 4ddbaaa2e2a..0e0206c2933 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -10,9 +10,10 @@ import {getGlobal} from '../../../src/prebidGlobal'; describe('adnuntiusBidAdapter', function() { const URL = 'https://ads.adnuntius.delivery/i?tzo='; const EURO_URL = 'https://europe.delivery.adnuntius.com/i?tzo='; - const GVLID = 855; const usi = utils.generateUUID() - const meta = [{key: 'usi', value: usi}] + + const meta = [{key: 'valueless'}, {value: 'keyless'}, {key: 'voidAuIds'}, {key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp()}, {exp: misc.getUnixTimestamp(1)}]}, {key: 'valid', value: 'also-valid', exp: misc.getUnixTimestamp(1)}, {key: 'expired', value: 'fwefew', exp: misc.getUnixTimestamp()}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}, {key: 'usi', value: usi, exp: misc.getUnixTimestamp(100)}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}] + let storage; before(() => { getGlobal().bidderSettings = { @@ -20,8 +21,11 @@ describe('adnuntiusBidAdapter', function() { storageAllowed: true } }; - const storage = getStorageManager({bidderCode: 'adnuntius'}) - storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + storage = getStorageManager({bidderCode: 'adnuntius'}); + }); + + beforeEach(() => { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)); }); after(() => { @@ -33,12 +37,12 @@ describe('adnuntiusBidAdapter', function() { }); const tzo = new Date().getTimezoneOffset(); - const ENDPOINT_URL_BASE = `${URL}${tzo}&format=json`; + const ENDPOINT_URL_BASE = `${URL}${tzo}&format=prebid`; const ENDPOINT_URL = `${ENDPOINT_URL_BASE}&userId=${usi}`; const ENDPOINT_URL_VIDEO = `${ENDPOINT_URL_BASE}&userId=${usi}&tt=vast4`; const ENDPOINT_URL_NOCOOKIE = `${ENDPOINT_URL_BASE}&userId=${usi}&noCookies=true`; const ENDPOINT_URL_SEGMENTS = `${ENDPOINT_URL_BASE}&segments=segment1,segment2,segment3&userId=${usi}`; - const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=json&consentString=consentString&userId=${usi}`; + const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=prebid&consentString=consentString&gdpr=1&userId=${usi}`; const adapter = newBidder(spec); const bidderRequests = [ @@ -47,6 +51,7 @@ describe('adnuntiusBidAdapter', function() { bidder: 'adnuntius', params: { auId: '000000000008b6bc', + targetId: '123', network: 'adnuntius', maxDeals: 1 }, @@ -99,7 +104,10 @@ describe('adnuntiusBidAdapter', function() { const videoBidRequest = { bid: videoBidderRequest, - bidder: 'adnuntius' + bidder: 'adnuntius', + params: { + bidType: 'justsomestuff-error-handling' + } } const deals = [ @@ -456,7 +464,78 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url'); expect(request[0].url).to.equal(ENDPOINT_URL); expect(request[0]).to.have.property('data'); - expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"adn-000000000008b6bc","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","maxDeals":0,"dimensions":[[1640,1480],[1600,1400]]}],"metaData":{"usi":"' + usi + '"}}'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}],"metaData":{"valid":"also-valid"}}'); + }); + + it('Test requests with no local storage', function() { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{}])); + const request = spec.buildRequests(bidderRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL_BASE); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}]}'); + + localStorage.removeItem('adn.metaData'); + const request2 = spec.buildRequests(bidderRequests, {}); + expect(request2.length).to.equal(1); + expect(request2[0]).to.have.property('url'); + expect(request2[0].url).to.equal(ENDPOINT_URL_BASE); + }); + + it('Test request changes for voided au ids', function() { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp(1)}, {auId: '0000000000000023', exp: misc.getUnixTimestamp(1)}]}])); + const bRequests = bidderRequests.concat([{ + bidId: 'adn-11118b6bc', + bidder: 'adnuntius', + params: { + auId: '11118b6bc', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + }]); + bRequests.push({ + bidId: 'adn-23', + bidder: 'adnuntius', + params: { + auId: '23', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + }); + bRequests.push({ + bidId: 'adn-13', + bidder: 'adnuntius', + params: { + auId: '13', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[164, 140], [10, 1400]], + } + }, + }); + const request = spec.buildRequests(bRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL_BASE); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]},{"auId":"13","targetId":"adn-13","dimensions":[[164,140],[10,1400]]}]}'); }); it('Test Video requests', function() { @@ -474,7 +553,7 @@ describe('adnuntiusBidAdapter', function() { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}] + segment: [{id: 'segment1'}, {id: 'segment2'}, {invalidSegment: 'invalid'}, {id: 123}, {id: ['3332']}] }, { name: 'other', @@ -581,6 +660,47 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL); }); + + it('should user in user', function () { + config.setBidderConfig({ + bidders: ['adnuntius'], + }); + const req = [ + { + bidId: 'adn-000000000008b6bc', + bidder: 'adnuntius', + params: { + auId: '000000000008b6bc', + network: 'adnuntius', + userId: 'different_user_id' + } + } + ] + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(req, { bids: req })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(`${ENDPOINT_URL_BASE}&userId=different_user_id`); + }); + + it('should handle no user specified', function () { + config.setBidderConfig({ + bidders: ['adnuntius'], + }); + const req = [ + { + bidId: 'adn-000000000008b6bc', + bidder: 'adnuntius', + params: { + auId: '000000000008b6bc', + network: 'adnuntius' + } + } + ] + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(req, { bids: req })); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(ENDPOINT_URL); + }); }); describe('user privacy', function() { @@ -657,7 +777,7 @@ describe('adnuntiusBidAdapter', function() { expect(bidderRequests[0].params.maxDeals).to.equal(1); expect(data.adUnits[0].maxDeals).to.equal(bidderRequests[0].params.maxDeals); expect(bidderRequests[1].params).to.not.have.property('maxBids'); - expect(data.adUnits[1].maxDeals).to.equal(0); + expect(data.adUnits[1].maxDeals).to.equal(undefined); }); it('Should allow a maximum of 5 deals.', function() { config.setBidderConfig({ @@ -703,7 +823,7 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('data'); const data = JSON.parse(request[0].data); expect(data.adUnits.length).to.equal(1); - expect(data.adUnits[0].maxDeals).to.equal(0); + expect(data.adUnits[0].maxDeals).to.equal(undefined); }); it('Should set max deals using bidder config.', function() { config.setBidderConfig({ @@ -749,12 +869,20 @@ describe('adnuntiusBidAdapter', function() { describe('interpretResponse', function() { it('should return valid response when passed valid server response', function() { - const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); + config.setBidderConfig({ + bidders: ['adnuntius'], + config: { + bidType: 'netBid', + maxDeals: 1 + } + }); + + const interpretedResponse = config.runWithBidder('adnuntius', () => spec.interpretResponse(serverResponse, singleBidRequest)); expect(interpretedResponse).to.have.lengthOf(2); const deal = serverResponse.body.adUnits[0].deals[0]; expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[0].cpm).to.equal(deal.bid.amount * 1000); + expect(interpretedResponse[0].cpm).to.equal(deal.netBid.amount * 1000); expect(interpretedResponse[0].width).to.equal(Number(deal.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(deal.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(deal.creativeId); @@ -770,7 +898,7 @@ describe('adnuntiusBidAdapter', function() { const ad = serverResponse.body.adUnits[0].ads[0]; expect(interpretedResponse[1].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[1].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[1].cpm).to.equal(ad.netBid.amount * 1000); expect(interpretedResponse[1].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[1].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[1].creativeId).to.equal(ad.creativeId); @@ -783,6 +911,33 @@ describe('adnuntiusBidAdapter', function() { expect(interpretedResponse[1].ttl).to.equal(360); expect(interpretedResponse[1].dealId).to.equal('not-in-deal-array-here'); expect(interpretedResponse[1].dealCount).to.equal(0); + + const results = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')); + const usiEntry = results.find(entry => entry.key === 'usi'); + expect(usiEntry.key).to.equal('usi'); + expect(usiEntry.value).to.equal('from-api-server dude'); + expect(usiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); + + const voidAuIdsEntry = results.find(entry => entry.key === 'voidAuIds'); + expect(voidAuIdsEntry.key).to.equal('voidAuIds'); + expect(voidAuIdsEntry.exp).to.equal(undefined); + expect(voidAuIdsEntry.value[0].auId).to.equal('00000000000abcde'); + expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + expect(voidAuIdsEntry.value[1].auId).to.equal('00000000000fffff'); + expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + + const validEntry = results.find(entry => entry.key === 'valid'); + expect(validEntry.key).to.equal('valid'); + expect(validEntry.value).to.equal('also-valid'); + expect(validEntry.exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(validEntry.exp).to.be.lessThan(misc.getUnixTimestamp(2)); + + const randomApiEntry = results.find(entry => entry.key === 'randomApiKey'); + expect(randomApiEntry.key).to.equal('randomApiKey'); + expect(randomApiEntry.value).to.equal('randomApiValue'); + expect(randomApiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); }); it('should not process valid response when passed alt bidder that is an adndeal', function() { @@ -795,6 +950,7 @@ describe('adnuntiusBidAdapter', function() { ] }; serverResponse.body.adUnits[0].deals = []; + delete serverResponse.body.metaData.voidAuIds; // test response with no voidAuIds const interpretedResponse = spec.interpretResponse(serverResponse, altBidder); expect(interpretedResponse).to.have.lengthOf(0); @@ -808,6 +964,9 @@ describe('adnuntiusBidAdapter', function() { { bidder: 'adn-alt', bidId: 'adn-0000000000000551', + params: { + bidType: 'netBid' + } } ] }; @@ -818,7 +977,7 @@ describe('adnuntiusBidAdapter', function() { const ad = serverResponse.body.adUnits[0].ads[0]; expect(interpretedResponse[0].bidderCode).to.equal('adn-alt'); - expect(interpretedResponse[0].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[0].cpm).to.equal(ad.netBid.amount * 1000); expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); diff --git a/test/spec/modules/adpod_spec.js b/test/spec/modules/adpod_spec.js index a6164f919ef..14e530c1a9b 100644 --- a/test/spec/modules/adpod_spec.js +++ b/test/spec/modules/adpod_spec.js @@ -47,7 +47,6 @@ describe('adpod.js', function () { addBidToAuctionStub = sinon.stub(auction, 'addBidToAuction').callsFake(function (auctionInstance, bid) { auctionBids.push(bid); }); - doCallbacksIfTimedoutStub = sinon.stub(auction, 'doCallbacksIfTimedout'); clock = sinon.useFakeTimers(); config.setConfig({ cache: { @@ -61,7 +60,6 @@ describe('adpod.js', function () { logWarnStub.restore(); logInfoStub.restore(); addBidToAuctionStub.restore(); - doCallbacksIfTimedoutStub.restore(); clock.restore(); config.resetConfig(); auctionBids = []; @@ -633,7 +631,6 @@ describe('adpod.js', function () { callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, videoMT); callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, videoMT); - expect(doCallbacksIfTimedoutStub.calledTwice).to.equal(true); expect(logWarnStub.calledOnce).to.equal(true); expect(auctionBids.length).to.equal(0); }); diff --git a/test/spec/modules/adqueryBidAdapter_spec.js b/test/spec/modules/adqueryBidAdapter_spec.js index e9286329d57..b4aa0992732 100644 --- a/test/spec/modules/adqueryBidAdapter_spec.js +++ b/test/spec/modules/adqueryBidAdapter_spec.js @@ -155,11 +155,39 @@ describe('adqueryBidAdapter', function () { describe('getUserSyncs', function () { it('should return iframe sync', function () { - let sync = spec.getUserSyncs() + let sync = spec.getUserSyncs( + { + iframeEnabled: true, + pixelEnabled: true, + }, + {}, + { + consentString: 'ALL', + gdprApplies: true, + }, + {} + ) expect(sync.length).to.equal(1) expect(sync[0].type === 'iframe') expect(typeof sync[0].url === 'string') }) + it('should return image sync', function () { + let sync = spec.getUserSyncs( + { + iframeEnabled: false, + pixelEnabled: true, + }, + {}, + { + consentString: 'ALL', + gdprApplies: true, + }, + {} + ) + expect(sync.length).to.equal(1) + expect(sync[0].type === 'image') + expect(typeof sync[0].url === 'string') + }) it('Should return array of objects with proper sync config , include GDPR', function() { const syncData = spec.getUserSyncs({}, {}, { diff --git a/test/spec/modules/adqueryIdSystem_spec.js b/test/spec/modules/adqueryIdSystem_spec.js index a6b4e9d1529..7952f23189e 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -1,5 +1,6 @@ -import { adqueryIdSubmodule, storage } from 'modules/adqueryIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; +import {adqueryIdSubmodule, storage} from 'modules/adqueryIdSystem.js'; +import {server} from 'test/mocks/xhr.js'; +import sinon from 'sinon'; const config = { storage: { @@ -18,10 +19,10 @@ describe('AdqueryIdSystem', function () { }); }); - describe('getId', function() { + describe('getId', function () { let getDataFromLocalStorageStub; - beforeEach(function() { + beforeEach(function () { getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); @@ -29,7 +30,7 @@ describe('AdqueryIdSystem', function () { getDataFromLocalStorageStub.restore(); }); - it('gets a adqueryId', function() { + it('gets a adqueryId', function () { const config = { params: {} }; @@ -37,36 +38,24 @@ describe('AdqueryIdSystem', function () { const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq(`https://bidder.adquery.io/prebid/qid`); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'qid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); + expect(request.url).to.contain(`https://bidder.adquery.io/prebid/qid`); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'qid_string' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('qid_string'); }); - it('gets a cached adqueryId', function() { - const config = { - params: {} - }; - getDataFromLocalStorageStub.withArgs('qid').returns('qid'); - - const callbackSpy = sinon.spy(); - const callback = adqueryIdSubmodule.getId(config).callback; - callback(callbackSpy); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); - }); - - it('allows configurable id url', function() { + it('allows configurable id url', function () { const config = { params: { - url: 'https://bidder.adquery.io' + url: 'https://bidder2.adquery.io' } }; const callbackSpy = sinon.spy(); const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://bidder.adquery.io'); + expect(request.url).to.contains('https://bidder2.adquery.io'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'testqid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'testqid'}); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('testqid'); }); }); }); diff --git a/test/spec/modules/adspiritBidAdapter_spec.js b/test/spec/modules/adspiritBidAdapter_spec.js new file mode 100644 index 00000000000..022a26da60e --- /dev/null +++ b/test/spec/modules/adspiritBidAdapter_spec.js @@ -0,0 +1,292 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adspiritBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { registerBidder } from 'src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from 'src/mediaTypes.js'; +const RTB_URL = '/rtb/getbid.php?rtbprovider=prebid'; +const SCRIPT_URL = '/adasync.min.js'; + +describe('Adspirit Bidder Spec', function () { + // isBidRequestValid ---case + describe('isBidRequestValid', function () { + it('should return true if the bid request is valid', function () { + const validBid = { bidder: 'adspirit', params: { placementId: '57', host: 'test.adspirit.de' } }; + const result = spec.isBidRequestValid(validBid); + expect(result).to.be.true; + }); + + it('should return false if the bid request is invalid', function () { + const invalidBid = { bidder: 'adspirit', params: {} }; + const result = spec.isBidRequestValid(invalidBid); + expect(result).to.be.false; + }); + }); + + // getBidderHost Case + describe('getBidderHost', function () { + it('should return host for adspirit bidder', function () { + const bid = { bidder: 'adspirit', params: { host: 'test.adspirit.de' } }; + const result = spec.getBidderHost(bid); + expect(result).to.equal('test.adspirit.de'); + }); + + it('should return host for twiago bidder', function () { + const bid = { bidder: 'twiago' }; + const result = spec.getBidderHost(bid); + expect(result).to.equal('a.twiago.com'); + }); + it('should return null for unsupported bidder', function () { + const bid = { bidder: 'unsupportedBidder', params: {} }; + const result = spec.getBidderHost(bid); + expect(result).to.be.null; + }); + }); + + // Test cases for buildRequests + describe('buildRequests', function () { + const bidRequestWithGDPRAndSchain = [ + { + id: '26c1ee0038ac11', + bidder: 'adspirit', + params: { + placementId: '57' + }, + schain: { + ver: '1.0', + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bidRequest123', + name: 'Publisher', + domain: 'publisher.com' + }, + { + asi: 'network1.com', + sid: '5678', + hp: 1, + rid: 'bidderRequest123', + name: 'Network', + domain: 'network1.com' + } + ] + } + } + ]; + + const mockBidderRequestWithGDPR = { + refererInfo: { + topmostLocation: 'test.adspirit.de' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'consentString' + }, + schain: { + ver: '1.0', + nodes: [ + { + asi: 'network1.com', + sid: '5678', + hp: 1, + rid: 'bidderRequest123', + name: 'Network', + domain: 'network1.com' + } + ] + } + }; + + it('should construct valid bid requests with GDPR consent and schain', function () { + const requests = spec.buildRequests(bidRequestWithGDPRAndSchain, mockBidderRequestWithGDPR); + expect(requests).to.be.an('array').that.is.not.empty; + const request = requests[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.include('test.adspirit.de'); + expect(request.url).to.include('pid=57'); + expect(request.data).to.have.property('schain'); + expect(request.data.schain).to.be.an('object'); + if (request.data.schain && Array.isArray(request.data.schain.nodes)) { + const nodeWithGdpr = request.data.schain.nodes.find(node => node.gdpr); + if (nodeWithGdpr) { + expect(nodeWithGdpr).to.have.property('gdpr'); + expect(nodeWithGdpr.gdpr).to.be.an('object'); + expect(nodeWithGdpr.gdpr).to.have.property('applies', true); + expect(nodeWithGdpr.gdpr).to.have.property('consent', 'consentString'); + } + } + }); + + it('should construct valid bid requests without GDPR consent and schain', function () { + const bidRequestWithoutGDPR = [ + { + id: '26c1ee0038ac11', + bidder: 'adspirit', + params: { + placementId: '57' + } + } + ]; + + const mockBidderRequestWithoutGDPR = { + refererInfo: { + topmostLocation: 'test.adspirit.de' + } + }; + + const requests = spec.buildRequests(bidRequestWithoutGDPR, mockBidderRequestWithoutGDPR); + expect(requests).to.be.an('array').that.is.not.empty; + const request = requests[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.include('test.adspirit.de'); + expect(request.url).to.include('pid=57'); + expect(request.data).to.deep.equal({}); + }); + }); + + // interpretResponse For Native + describe('interpretResponse', function () { + const nativeBidRequestMock = { + bidRequest: { + bidId: '123456', + params: { + placementId: '57', + adomain: ['test.adspirit.de'] + }, + mediaTypes: { + native: true + } + } + }; + + it('should handle native media type bids and missing cpm in the server response body', function () { + const serverResponse = { + body: { + w: 320, + h: 50, + title: 'Ad Title', + body: 'Ad Body', + cta: 'Click Here', + image: 'img_url', + click: 'click_url', + view: 'view_tracker_url' + } + }; + + const result = spec.interpretResponse(serverResponse, nativeBidRequestMock); + expect(result.length).to.equal(0); + }); + + it('should handle native media type bids', function () { + const serverResponse = { + body: { + cpm: 1.0, + w: 320, + h: 50, + title: 'Ad Title', + body: 'Ad Body', + cta: 'Click Here', + image: 'img_url', + click: 'click_url', + view: 'view_tracker_url' + } + }; + + const result = spec.interpretResponse(serverResponse, nativeBidRequestMock); + expect(result.length).to.equal(1); + const bid = result[0]; + expect(bid).to.include({ + requestId: '123456', + cpm: 1.0, + width: 320, + height: 50, + creativeId: '57', + currency: 'EUR', + netRevenue: true, + ttl: 300, + mediaType: 'native' + }); + expect(bid.native).to.deep.include({ + title: 'Ad Title', + body: 'Ad Body', + cta: 'Click Here', + image: { url: 'img_url' }, + clickUrl: 'click_url', + impressionTrackers: ['view_tracker_url'] + }); + }); + + const bannerBidRequestMock = { + bidRequest: { + bidId: '123456', + params: { + placementId: '57', + adomain: ['siva.adspirit.de'] + }, + mediaTypes: { + banner: true + } + } + }; + + // Test cases for various scenarios + it('should return empty array when serverResponse is missing', function () { + const result = spec.interpretResponse(null, { bidRequest: {} }); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return empty array when serverResponse.body is missing', function () { + const result = spec.interpretResponse({}, { bidRequest: {} }); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return empty array when bidObj is missing', function () { + const result = spec.interpretResponse({ body: { cpm: 1.0 } }, { bidRequest: null }); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should return empty array when all required parameters are missing', function () { + const result = spec.interpretResponse(null, { bidRequest: null }); + expect(result).to.be.an('array').that.is.empty; + }); + + it('should handle banner media type bids and missing cpm in the server response body', function () { + const serverResponseBanner = { + body: { + w: 728, + h: 90, + adm: '
Ad Content
' + } + }; + const result = spec.interpretResponse(serverResponseBanner, bannerBidRequestMock); + expect(result.length).to.equal(0); + }); + + it('should handle banner media type bids', function () { + const serverResponse = { + body: { + cpm: 2.0, + w: 728, + h: 90, + adm: '
Ad Content
' + } + }; + const result = spec.interpretResponse(serverResponse, bannerBidRequestMock); + expect(result.length).to.equal(1); + const bid = result[0]; + expect(bid).to.include({ + requestId: '123456', + cpm: 2.0, + width: 728, + height: 90, + creativeId: '57', + currency: 'EUR', + netRevenue: true, + ttl: 300, + mediaType: 'banner' + }); + expect(bid.ad).to.equal('
Ad Content
'); + }); + }); +}); diff --git a/test/spec/modules/adstirBidAdapter_spec.js b/test/spec/modules/adstirBidAdapter_spec.js new file mode 100644 index 00000000000..a62dce8af97 --- /dev/null +++ b/test/spec/modules/adstirBidAdapter_spec.js @@ -0,0 +1,413 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/adstirBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; + +describe('AdstirAdapter', function () { + describe('isBidRequestValid', function () { + it('should return true if appId is non-empty string and adSpaceNo is integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false if appId is non-empty string, but adSpaceNo is not integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 'a', + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is null', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: null, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is undefined', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX' + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is empty string', function () { + const bid = { + params: { + appId: '', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is not string', function () { + const bid = { + params: { + appId: 123, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is null', function () { + const bid = { + params: { + appId: null, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is undefined', function () { + const bid = { + params: { + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if params is empty', function () { + const bid = { + params: {} + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const validBidRequests = [ + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid1111', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 1, + }, + transactionId: 'transactionid-1111', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + } + }, + sizes: [ + [300, 250], + [336, 280], + ], + }, + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid2222', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 2, + }, + transactionId: 'transactionid-2222', + mediaTypes: { + banner: { + sizes: [ + [320, 50], + [320, 100], + ], + } + }, + sizes: [ + [320, 50], + [320, 100], + ], + }, + ]; + + const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: 'https://ad-stir.com/contact', + topmostLocation: 'https://ad-stir.com/contact', + reachedTop: true, + ref: 'https://test.example/q=adstir', + isAmp: false, + numIframes: 0, + stack: [ + 'https://ad-stir.com/contact', + ], + }, + }; + + it('one entry in validBidRequests corresponds to one server request object.', function () { + const requests = spec.buildRequests(validBidRequests, bidderRequest); + expect(requests.length).to.equal(validBidRequests.length); + requests.forEach(function (r, i) { + expect(r.method).to.equal('POST'); + expect(r.url).to.equal('https://ad.ad-stir.com/prebid'); + const d = JSON.parse(r.data); + expect(d.auctionId).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); + + const v = validBidRequests[i]; + expect(d.appId).to.equal(v.params.appId); + expect(d.adSpaceNo).to.equal(v.params.adSpaceNo); + expect(d.bidId).to.equal(v.bidId); + expect(d.transactionId).to.equal(v.transactionId); + expect(d.mediaTypes).to.deep.equal(v.mediaTypes); + expect(d.sizes).to.deep.equal(v.sizes); + expect(d.ref.page).to.equal(bidderRequest.refererInfo.page); + expect(d.ref.tloc).to.equal(bidderRequest.refererInfo.topmostLocation); + expect(d.ref.referrer).to.equal(bidderRequest.refererInfo.ref); + expect(d.sua).to.equal(null); + expect(d.user).to.equal(null); + expect(d.gdpr).to.equal(false); + expect(d.usp).to.equal(false); + expect(d.schain).to.equal(null); + expect(d.eids).to.deep.equal([]); + }); + }); + + it('ref.page, ref.tloc and ref.referrer correspond to refererInfo', function () { + const [ request ] = spec.buildRequests([validBidRequests[0]], { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: null, + topmostLocation: 'https://adserver.example/iframe1.html', + reachedTop: false, + ref: null, + isAmp: false, + numIframes: 2, + stack: [ + null, + 'https://adserver.example/iframe1.html', + 'https://adserver.example/iframe2.html' + ], + }, + }); + + const { ref } = JSON.parse(request.data); + expect(ref.page).to.equal(null); + expect(ref.tloc).to.equal('https://adserver.example/iframe1.html'); + expect(ref.referrer).to.equal(null); + }); + + it('when config.pageUrl is not set, ref.topurl equals to refererInfo.reachedTop', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(reachedTop); + }); + }); + + describe('when config.pageUrl is set, ref.topurl is always false', function () { + before(function () { + config.setConfig({ pageUrl: 'https://ad-stir.com/register' }); + }); + after(function () { + config.resetConfig(); + }); + + it('ref.topurl should be false', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(false); + }); + }); + }); + + it('gdprConsent.gdprApplies is sent', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (gdprApplies) { + bidderRequestClone.gdprConsent = { gdprApplies }; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.gdpr).to.equal(gdprApplies); + }); + }); + + it('includes in the request parameters whether CCPA applies', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + const cases = [ + { uspConsent: '1---', expected: false }, + { uspConsent: '1YYY', expected: true }, + { uspConsent: '1YNN', expected: true }, + { uspConsent: '1NYN', expected: true }, + { uspConsent: '1-Y-', expected: true }, + ]; + cases.forEach(function ({ uspConsent, expected }) { + bidderRequestClone.uspConsent = uspConsent; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.usp).to.equal(expected); + }); + }); + + it('should add schain if available', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher, Inc.', + 'domain': 'publisher.example' + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.example' + } + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,bid-request-1,publisher%2C%20Inc.,publisher.example!exchange2.example,abcd,1,bid-request-2,intermediary,intermediary.example'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add schain even if some nodes params are blank', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + }, + { + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + }, + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,,,!,,,,,!exchange2.example,abcd,1,,,'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add UA client hints to payload if available', function () { + const sua = { + browsers: [ + { + brand: 'Not?A_Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + version: [ + '108', + '0', + '5359', + '40' + ] + }, + { + brand: 'Google Chrome', + version: [ + '108', + '0', + '5359', + '40' + ] + } + ], + platform: { + brand: 'Android', + version: [ + '11' + ] + }, + mobile: 1, + architecture: '', + bitness: '64', + model: 'Pixel 5', + source: 2 + } + + const validBidRequestsClone = utils.deepClone(validBidRequests); + validBidRequestsClone[0] = utils.mergeDeep(validBidRequestsClone[0], { + ortb2: { + device: { sua }, + } + }); + + const requests = spec.buildRequests(validBidRequestsClone, bidderRequest); + requests.forEach(function (r) { + const d = JSON.parse(r.data); + expect(d.sua).to.deep.equal(sua); + }); + }); + }); + + describe('interpretResponse', function () { + it('return empty array when no content', function () { + const bids = spec.interpretResponse({ body: '' }); + expect(bids).to.deep.equal([]); + }); + it('return empty array when seatbid empty', function () { + const bids = spec.interpretResponse({ body: { seatbid: [] } }); + expect(bids).to.deep.equal([]); + }); + it('return valid bids when serverResponse is valid', function () { + const serverResponse = { + 'body': { + 'seatbid': [ + { + 'bid': { + 'ad': '
test response
', + 'cpm': 5250, + 'creativeId': '5_1234ABCD', + 'currency': 'JPY', + 'height': 250, + 'meta': { + 'advertiserDomains': [ + 'adv.example' + ], + 'mediaType': 'banner', + 'networkId': 5 + }, + 'netRevenue': true, + 'requestId': '22a9457aed98a4', + 'transactionId': 'f18c078e-4d2a-4ecb-a886-2a0c52187213', + 'ttl': 60, + 'width': 300, + } + } + ] + }, + 'headers': {} + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids[0]).to.deep.equal(serverResponse.body.seatbid[0].bid); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index e40828e6852..0acbaa06f5b 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,18 +11,13 @@ const EXPECTED_ENDPOINTS = [ '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/', - 'navelix': 'https://ghb.hb.navelix.com/v2/auction/', - 'bidsxchange': 'https://ghb.hbd.bidsxchange.com/v2/auction/', + 'janet_publisherSuffix': 'https://ghb.bidder.jmgads.com/v2/auction/', 'streamkey': 'https://ghb.hb.streamkey.net/v2/auction/', 'janet': 'https://ghb.bidder.jmgads.com/v2/auction/', - 'pgam': 'https://ghb.pgamssp.com/v2/auction/', 'ocm': 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', - 'vidcrunchllc': 'https://ghb.platform.vidcrunch.com/v2/auction/', '9dotsmedia': 'https://ghb.platform.audiodots.com/v2/auction/', 'copper6': 'https://ghb.app.copper6.com/v2/auction/', + 'indicue': 'https://ghb.console.indicue.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 65c7584b428..e07e3a6e5d4 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,835 +1,19 @@ // jshint esversion: 6, es3: false, node: true -import {assert} from 'chai'; -import {spec} from 'modules/adxcgBidAdapter.js'; -import {config} from 'src/config.js'; -import {createEidsArray} from 'modules/userId/eids.js'; +import { assert } from 'chai'; +import { spec } from 'modules/adxcgBidAdapter.js'; +import { config } from 'src/config.js'; +import { createEidsArray } from 'modules/userId/eids.js'; +/* eslint dot-notation:0, quote-props:0 */ +import { expect } from 'chai'; + +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; +import { deepClone } from '../../../src/utils'; + const utils = require('src/utils'); describe('Adxcg adapter', function () { let bids = []; - describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'adxcg', - 'params': { - 'adzoneid': '19910113' - } - }; - - it('should return true when required params found', function () { - assert(spec.isBidRequestValid(bid)); - - bid.params = { - adzoneid: 4332, - }; - assert(spec.isBidRequestValid(bid)); - }); - - it('should return false when required params are missing', function () { - bid.params = {}; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - mname: 'some-placement' - }; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - inv: 1234 - }; - assert.isFalse(spec.isBidRequestValid(bid)); - }); - }); - - describe('buildRequests', function () { - beforeEach(function () { - config.resetConfig(); - }); - it('should send request with correct structure', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: { - adzoneid: '19910113' - } - }]; - let request = spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}); - - assert.equal(request.method, 'POST'); - assert.equal(request.url, 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); - assert.deepEqual(request.options, {contentType: 'application/json'}); - assert.ok(request.data); - }); - - describe('user privacy', function () { - it('should send GDPR Consent data to exchange if gdprApplies', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, bidderRequest.gdprConsent.consentString); - assert.equal(request.regs.ext.gdpr, bidderRequest.gdprConsent.gdprApplies); - assert.equal(typeof request.regs.ext.gdpr, 'number'); - }); - - it('should send gdpr as number', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(typeof request.regs.ext.gdpr, 'number'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should send CCPA Consent data to exchange', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = {uspConsent: '1YA-', refererInfo: {referer: 'page'}}; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - - bidderRequest = { - uspConsent: '1YA-', - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should not send GDPR Consent data to adxcg if gdprApplies is undefined', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let bidderRequest = { - gdprConsent: {gdprApplies: false, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 0); - - bidderRequest = {gdprConsent: {consentString: 'consentDataString'}, refererInfo: {referer: 'page'}}; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - it('should send default GDPR Consent data to exchange', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - }); - - it('should add test and is_debug to request, if test is set in parameters', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {test: 1} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.ok(request.is_debug); - assert.equal(request.test, 1); - }); - - it('should have default request structure', function () { - let keys = 'site,geo,device,source,ext,imp'.split(','); - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - let data = Object.keys(request); - - assert.deepEqual(keys, data); - }); - - it('should set request keys correct values', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'}, - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, { - refererInfo: {referer: 'page'}, - ortb2: {source: {tid: 'tid'}} - }).data); - - assert.equal(request.source.tid, 'tid'); - assert.equal(request.source.fd, 1); - }); - - it('should send info about device', function () { - config.setConfig({ - device: {w: 100, h: 100} - }); - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}).data); - - assert.equal(request.device.ua, navigator.userAgent); - assert.equal(request.device.w, 100); - assert.equal(request.device.h, 100); - }); - - it('should send app info', function () { - config.setConfig({ - app: {id: 'appid'}, - }); - const ortb2 = {app: {name: 'appname'}} - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}, ortb2}).data); - - assert.equal(request.app.id, 'appid'); - assert.equal(request.app.name, 'appname'); - assert.equal(request.site, undefined); - }); - - it('should send info about the site', function () { - config.setConfig({ - site: { - id: '123123', - publisher: { - domain: 'publisher.domain.com' - } - }, - }); - const ortb2 = { - site: { - publisher: { - id: 4441, - name: 'publisher\'s name' - } - } - }; - - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let refererInfo = {page: 'page', domain: 'localhost'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo, ortb2}).data); - - assert.deepEqual(request.site, { - domain: 'localhost', - id: '123123', - page: refererInfo.page, - publisher: { - domain: 'publisher.domain.com', - id: 4441, - name: 'publisher\'s name' - } - }); - }); - - it('should pass extended ids', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }) - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.user.ext.eids, [ - {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} - ]); - }); - - it('should send currency if defined', function () { - config.setConfig({currency: {adServerCurrency: 'EUR'}}); - let validBidRequests = [{params: {}}]; - let refererInfo = {referer: 'page'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo}).data); - - assert.deepEqual(request.cur, ['EUR']); - }); - - it('should pass supply chain object', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - schain: { - validation: 'strict', - config: { - ver: '1.0' - } - } - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.source.ext.schain, { - validation: 'strict', - config: { - ver: '1.0' - } - }); - }); - - describe('bids', function () { - it('should add more than one bid to the request', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }, { - bidId: 'bidId2', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.imp.length, 2); - }); - it('should add incrementing values of id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId2', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId3', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].id, i + 1); - } - }); - - it('should add adzoneid', function () { - let validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}, - {bidId: 'bidId2', params: {adzoneid: 1001}, mediaTypes: {video: {}}}, - {bidId: 'bidId3', params: {adzoneid: 1002}, mediaTypes: {video: {}}}]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].tagid, validBidRequests[i].params.adzoneid); - } - }); - - describe('price floors', function () { - it('should not add if floors module not configured', function () { - const validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, undefined); - }); - - it('should not add if floor price not defined', function () { - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'USD'); - }); - - it('should request floor price in adserver currency', function () { - config.setConfig({currency: {adServerCurrency: 'DKK'}}); - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'DKK'); - }); - - it('should add correct floor values', function () { - const expectedFloors = [1, 1.3, 0.5]; - const validBidRequests = expectedFloors.map(getBidWithFloor); - let imps = getRequestImps(validBidRequests); - - expectedFloors.forEach((floor, index) => { - assert.equal(imps[index].bidfloor, floor); - assert.equal(imps[index].bidfloorcur, 'USD'); - }); - }); - - function getBidWithFloor(floor) { - return { - params: {adzoneid: 1}, - mediaTypes: {video: {}}, - getFloor: ({currency}) => { - return { - currency: currency, - floor - }; - } - }; - } - }); - - describe('multiple media types', function () { - it('should use all configured media types for bidding', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - video: {} - } - }, { - bidId: 'bidId1', - params: {adzoneid: 1000}, - mediaTypes: { - video: {}, - native: {} - } - }, { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140} - }, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - native: {}, - video: {} - } - }]; - let [first, second, third] = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - assert.ok(first.banner); - assert.ok(first.video); - assert.equal(first.native, undefined); - - assert.ok(second.video); - assert.equal(second.banner, undefined); - assert.equal(second.native, undefined); - - assert.ok(third.native); - assert.ok(third.video); - assert.ok(third.banner); - }); - }); - - describe('banner', function () { - it('should convert sizes to openrtb format', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - } - } - }]; - let {banner} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(banner, { - format: [{w: 100, h: 100}, {w: 200, h: 300}] - }); - }); - }); - - describe('video', function () { - it('should pass video mediatype config', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - } - } - }]; - let {video} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(video, { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - }); - }); - }); - - describe('native', function () { - describe('assets', function () { - it('should set correct asset id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }]; - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].id, 0); - assert.equal(assets[1].id, 3); - assert.equal(assets[2].id, 4); - }); - it('should add required key if it is necessary', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140}, - sponsoredBy: {required: true, len: 140} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].required, 1); - assert.ok(!assets[1].required); - assert.ok(!assets[2].required); - assert.equal(assets[3].required, 1); - }); - - it('should map img and data assets', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: true, sizes: [150, 50]}, - icon: {required: false, sizes: [50, 50]}, - body: {required: false, len: 140}, - sponsoredBy: {required: true}, - cta: {required: false}, - clickUrl: {required: false} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].title); - assert.equal(assets[0].title.len, 140); - assert.deepEqual(assets[1].img, {type: 3, w: 150, h: 50}); - assert.deepEqual(assets[2].img, {type: 1, w: 50, h: 50}); - assert.deepEqual(assets[3].data, {type: 2, len: 140}); - assert.deepEqual(assets[4].data, {type: 1}); - assert.deepEqual(assets[5].data, {type: 12}); - assert.ok(!assets[6]); - }); - - describe('icon/image sizing', function () { - it('should flatten sizes and utilise first pair', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - sizes: [[200, 300], [100, 200]] - }, - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.w, 200); - assert.equal(assets[0].img.h, 300); - }); - }); - - it('should utilise aspect_ratios', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - min_width: 100, - ratio_height: 3, - ratio_width: 1 - }] - }, - icon: { - aspect_ratios: [{ - min_width: 10, - ratio_height: 5, - ratio_width: 2 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 100); - assert.equal(assets[0].img.hmin, 300); - - assert.ok(assets[1].img); - assert.equal(assets[1].img.wmin, 10); - assert.equal(assets[1].img.hmin, 25); - }); - - it('should not throw error if aspect_ratios config is not defined', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [] - }, - icon: { - aspect_ratios: [] - } - } - }]; - - assert.doesNotThrow(() => spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}})); - }); - }); - - it('should expect any dimensions if min_width not passed', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - ratio_height: 3, - ratio_width: 1 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 0); - assert.equal(assets[0].img.hmin, 0); - assert.ok(!assets[1]); - }); - }); - }); - - function getRequestImps(validBidRequests) { - return JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - } - }); - - describe('interpretResponse', function () { - it('should return if no body in response', function () { - let serverResponse = {}; - let bidRequest = {}; - - assert.ok(!spec.interpretResponse(serverResponse, bidRequest)); - }); - it('should return more than one bids', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{ - impid: '1', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, title: {text: 'Asset title text'}}]} - }] - }, { - bid: [{ - impid: '2', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, data: {value: 'Asset title text'}}]} - }] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }, - { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(spec.interpretResponse(serverResponse, bidRequest).length, 2); - }); - - it('should set correct values to bid', function () { - let nativeExample1 = { - assets: [], - link: {url: 'link'}, - imptrackers: ['imptrackers url1', 'imptrackers url2'] - } - - let serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{ - bid: [ - { - impid: '1', - price: 93.1231, - crid: '12312312', - adm: JSON.stringify(nativeExample1), - dealid: 'deal-id', - adomain: ['demo.com'], - ext: { - crType: 'native', - advertiser_id: 'adv1', - advertiser_name: 'advname', - agency_name: 'agname', - mediaType: 'native' - } - } - ] - }], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - const bids = spec.interpretResponse(serverResponse, bidRequest); - const bid = serverResponse.body.seatbid[0].bid[0]; - assert.deepEqual(bids[0].requestId, bidRequest.bids[0].bidId); - assert.deepEqual(bids[0].cpm, bid.price); - assert.deepEqual(bids[0].creativeId, bid.crid); - assert.deepEqual(bids[0].ttl, 300); - assert.deepEqual(bids[0].netRevenue, false); - assert.deepEqual(bids[0].currency, serverResponse.body.cur); - assert.deepEqual(bids[0].mediaType, 'native'); - assert.deepEqual(bids[0].meta.mediaType, 'native'); - assert.deepEqual(bids[0].meta.advertiserDomains, ['demo.com']); - - assert.deepEqual(bids[0].meta.advertiserName, 'advname'); - assert.deepEqual(bids[0].meta.agencyName, 'agname'); - - assert.deepEqual(bids[0].dealId, 'deal-id'); - }); - - it('should return empty when there is no bids in response', function () { - const serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{bid: []}], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [{bidId: 'bidId1'}] - }; - const result = spec.interpretResponse(serverResponse, bidRequest)[0]; - assert.ok(!result); - }); - - describe('banner', function () { - it('should set ad content on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'banner'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].ad, ''); - assert.equal(bids[0].mediaType, 'banner'); - assert.equal(bids[0].meta.mediaType, 'banner'); - }); - }); - - describe('video', function () { - it('should set vastXml on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'video'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].vastXml, ''); - assert.equal(bids[0].mediaType, 'video'); - assert.equal(bids[0].meta.mediaType, 'video'); - }); - }); - }); - describe('getUserSyncs', function () { const usersyncUrl = 'https://usersync-url.com'; beforeEach(() => { @@ -846,55 +30,55 @@ describe('Adxcg adapter', function () { }) it('should return user sync if pixel enabled with adxcg config', function () { - const ret = spec.getUserSyncs({pixelEnabled: true}) - expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + const ret = spec.getUserSyncs({ pixelEnabled: true }) + expect(ret).to.deep.equal([{ type: 'image', url: usersyncUrl }]) }) it('should not return user sync if pixel disabled', function () { - const ret = spec.getUserSyncs({pixelEnabled: false}) + 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}) + const ret = spec.getUserSyncs({ pixelEnabled: true }) expect(ret).to.be.an('array').that.is.empty }) - it('should pass GDPR consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + it('should pass GDPR consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` }]); }); - it('should pass US consent', function() { + it('should pass US consent', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` }]); }); - it('should pass GDPR and US consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + it('should pass GDPR and US consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); }); - describe('onBidWon', function() { - beforeEach(function() { + describe('onBidWon', function () { + beforeEach(function () { sinon.stub(utils, 'triggerPixel'); }); - afterEach(function() { + afterEach(function () { utils.triggerPixel.restore(); }); - it('Should trigger pixel if bid nurl', function() { + it('Should trigger pixel if bid nurl', function () { const bid = { nurl: 'http://example.com/win/${AUCTION_PRICE}', cpm: 2.1, @@ -904,4 +88,510 @@ describe('Adxcg adapter', function () { expect(utils.triggerPixel.callCount).to.equal(1) }) }) + + it('should return just to have at least 1 karma test ok', function () { + assert(true); + }); +}); + +describe('adxcg v8 oRtbConverter Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '300x250', + adzoneid: '77' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + params: { + cp: 'p10000', + ct: 't20000', + cf: '728x90', + adzoneid: '77' + } + }]; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 0, + data: { + type: 1 + } + }] + }; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaTypes: { + native: { + sendTargetingKeys: false, + ortb: nativeOrtbRequest + } + }, + nativeOrtbRequest, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slotVideo', + bidId: 'bid12345', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const additionalParamsConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345, + extra_key3: { + key1: 'val1', + key2: 23456, + }, + extra_key4: [1, 2, 3] + } + }]; + + const schainParamsSlotConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + bcat: ['IAB-1', 'IAB-20'], + battr: [1, 2, 3], + bidfloor: 1.5, + badv: ['cocacola.com', 'lays.com'] + }, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } + }; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + // expect(ortbRequest.site.publisher.id).to.equal('p10000'); + expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + // expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }, { 'w': 160, 'h': 600 }]); + // slot 2 + // expect(ortbRequest.imp[1].tagid).to.equal('t20000'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }]); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + mtype: 1, + w: 300, + h: 250, + exp: 20, + adomain: ['advertiser.com'] + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creative_id).to.equal('Creative#123'); + expect(bid.creativeId).to.equal('Creative#123'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('EUR'); + expect(bid.ttl).to.equal(20); + expect(bid.meta).to.not.be.null; + expect(bid.meta.advertiserDomains).to.eql(['advertiser.com']); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs, bidderRequest); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + if (FEATURES.NATIVE) { + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('77'); + expect(ortbRequest.imp[0].banner).to.be.undefined; + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(3); + // image asset + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.be.undefined; + expect(nativeRequest.assets[0].img).to.not.equal(null); + expect(nativeRequest.assets[0].img.w).to.equal(150); + expect(nativeRequest.assets[0].img.h).to.equal(50); + expect(nativeRequest.assets[0].img.type).to.equal(3); + // title asset + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[1].required).to.equal(1); + expect(nativeRequest.assets[1].title).to.not.equal(null); + expect(nativeRequest.assets[1].title.len).to.equal(80); + // data asset + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[2].title).to.be.undefined; + expect(nativeRequest.assets[2].data).to.not.equal(null); + expect(nativeRequest.assets[2].data.type).to.equal(1); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const nativeResponse = { + assets: [ + { id: 1, img: { type: 3, url: 'https://images.cdn.brand.com/123' } }, + { id: 2, title: { text: 'Ad Title' } }, + { id: 3, data: { type: 1, value: 'Sponsored By: Brand' } } + ], + link: { url: 'https://brand.clickme.com/' }, + imptrackers: ['https://imp1.trackme.com/', 'https://imp1.contextweb.com/'] + + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: JSON.stringify(nativeResponse), + mtype: 4 + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + expect(bid['native']).to.not.be.null; + expect(bid['native'].ortb).to.not.be.null; + const nativeBid = bid['native'].ortb; + expect(nativeBid.assets).to.have.lengthOf(3); + expect(nativeBid.assets[0].id).to.equal(1); + expect(nativeBid.assets[0].img).to.not.be.null; + expect(nativeBid.assets[0].img.type).to.equal(3); + expect(nativeBid.assets[0].img.url).to.equal('https://images.cdn.brand.com/123'); + expect(nativeBid.assets[1].id).to.equal(2); + expect(nativeBid.assets[1].title).to.not.be.null; + expect(nativeBid.assets[1].title.text).to.equal('Ad Title'); + expect(nativeBid.assets[2].id).to.equal(3); + expect(nativeBid.assets[2].data).to.not.be.null; + expect(nativeBid.assets[2].data.type).to.equal(1); + expect(nativeBid.assets[2].data.value).to.equal('Sponsored By: Brand'); + expect(nativeBid.link).to.not.be.null; + expect(nativeBid.link.url).to.equal('https://brand.clickme.com/'); + expect(nativeBid.imptrackers).to.have.lengthOf(2); + expect(nativeBid.imptrackers[0]).to.equal('https://imp1.trackme.com/'); + expect(nativeBid.imptrackers[1]).to.equal('https://imp1.contextweb.com/'); + }); + } + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('adxcg'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('mediaopti'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + if (FEATURES.VIDEO) { + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video).to.not.be.null; + expect(ortbRequest.imp[0].native).to.be.undefined; + expect(ortbRequest.imp[0].banner).to.be.undefined; + expect(ortbRequest.imp[0].video.w).to.equal(400); + expect(ortbRequest.imp[0].video.h).to.equal(300); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.maxduration).to.equal(10); + expect(ortbRequest.imp[0].video.startdelay).to.equal(0); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); + expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); + }); + } + + it('Verify extra parameters', function () { + let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key1).to.equal('extra_val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key2).to.equal(12345); + expect(ortbRequest.imp[0].ext.prebid.extra_key3).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key1).to.equal('val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key2).to.equal(23456); + expect(ortbRequest.imp[0].ext.prebid.extra_key4).to.eql([1, 2, 3]); + expect(Object.keys(ortbRequest.imp[0].ext.prebid)).to.eql(['adzoneid', 'extra_key1', 'extra_key2', 'extra_key3', 'extra_key4']); + // attempting with a configuration with no unknown params. + request = spec.buildRequests(videoSlotConfig, bidderRequest); + ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + // expect(ortbRequest.imp[0].ext).to.be.undefined; + }); + + it('Verify user level first party data', function () { + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + }, + ortb2: { + user: { + yob: 1985, + gender: 'm', + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.user).to.not.equal(null); + }); + + it('Verify site level first party data', function () { + const bidderRequest = { + ortb2: { + site: { + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + domain: 'pub.com' + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site).to.deep.equal({ + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + // id: 'p10000', + domain: 'pub.com' + } + }); + }); + + it('Verify impression/slot level first party data', function () { + const bidderRequests = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + } + } + }]; + let request = spec.buildRequests(bidderRequests, bidderRequest); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext).to.deep.equal({ + prebid: { + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + }); + }); + + it('Verify bid request timeouts', function () { + const mkRequest = (bidderRequest) => spec.buildRequests(slotConfigs, bidderRequest).data; + // assert default is used when no bidderRequest.timeout value is available + expect(mkRequest(bidderRequest).tmax).to.equal(500) + + // assert bidderRequest value is used when available + expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) + }); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 7310f736f7e..ffd6729397a 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adyoulikeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; describe('Adyoulike Adapter', function () { const canonicalUrl = 'https://canonical.url/?t=%26'; @@ -707,32 +708,21 @@ describe('Adyoulike Adapter', function () { expect(payload.gdprConsent.consentRequired).to.be.null; }); - it('should add userid eids information to the request', function () { - let bidderRequest = { - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000, - 'userIdAsEids': - [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'atype': 1, - 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' - } - ] - } - ] - }; - - bidderRequest.bids = bidRequestWithSinglePlacement; - - const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + it('should add eids eids information to the request', function () { + let bidRequest = bidRequestWithSinglePlacement; + bidRequest[0].userIdAsEids = [{ + 'source': 'pubcid.org', + 'uids': [{ + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + }] + }] + + const request = spec.buildRequests(bidRequest, bidderRequest); const payload = JSON.parse(request.data); - expect(payload.userId).to.exist; - expect(payload.userId).to.deep.equal(bidderRequest.userIdAsEids); + expect(payload.eids).to.exist; + expect(payload.eids).to.deep.equal(bidRequest[0].userIdAsEids); }); it('sends bid request to endpoint with single placement', function () { @@ -898,4 +888,115 @@ describe('Adyoulike Adapter', function () { expect(spec.gvlid).to.equal(259) }) }); + + describe('getUserSyncs', function () { + const syncurl_iframe = 'https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4'; + + const emptySync = []; + + describe('with iframe enabled', function() { + const userSyncConfig = { iframeEnabled: true }; + + it('should not add parameters if not provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should add GDPR parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo?'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo%3F` + }]); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: false, consentString: 'bar'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=bar` + }]); + }); + + it('should add CCPA parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, 'foo?')).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&us_privacy=foo%3F` + }]); + }); + + describe('COPPA', function() { + let sandbox; + + this.beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + this.afterEach(function() { + sandbox.restore(); + }); + + it('should add coppa parameters if provided', function() { + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': true + }; + return config[key]; + }); + + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&coppa=1` + }]); + }); + }); + + describe('GPP', function() { + it('should not apply if not gppConsent.gppString', function() { + const gppConsent = { gppString: '', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if not gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: undefined }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if empty gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: [] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should apply if all above are available', function() { + const gppConsent = { gppString: 'foo?', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo%3F&gpp_sid=123` + }]); + }); + + it('should support multiple sections', function() { + const gppConsent = { gppString: 'foo', applicableSections: [123, 456] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo&gpp_sid=123%2C456` + }]); + }); + }); + }); + + describe('with iframe disabled', function() { + const userSyncConfig = { iframeEnabled: false }; + + it('should return empty list of syncs', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal(emptySync); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo'}, 'bar')).to.deep.equal(emptySync); + }); + }); + }); }); diff --git a/test/spec/modules/agmaAnalyticsAdapter_spec.js b/test/spec/modules/agmaAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..df18e7bcf45 --- /dev/null +++ b/test/spec/modules/agmaAnalyticsAdapter_spec.js @@ -0,0 +1,392 @@ +import adapterManager from '../../../src/adapterManager.js'; +import agmaAnalyticsAdapter, { + getTiming, + getOrtb2Data, + getPayload, +} from '../../../modules/agmaAnalyticsAdapter.js'; +import { gdprDataHandler } from '../../../src/adapterManager.js'; +import { expect } from 'chai'; +import * as events from '../../../src/events.js'; +import constants from '../../../src/constants.json'; +import { generateUUID } from '../../../src/utils.js'; +import { server } from '../../mocks/xhr.js'; +import { config } from 'src/config.js'; + +const INGEST_URL = 'https://pbc.agma-analytics.de/v1'; +const extendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'extended', + 'gdprApplies', + 'gdprConsentString', + 'language', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'referrer', + 'screenHeight', + 'screenWidth', + 'deviceWidth', + 'deviceHeight', + 'scriptVersion', + 'timestamp', + 'timezoneOffset', + 'timing', + 'triggerEvent', + 'userIdsAsEids', +]; +const nonExtendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'gdprApplies', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'scriptVersion', + 'timing', + 'triggerEvent', +]; + +describe('AGMA Analytics Adapter', () => { + let agmaConfig, sandbox, clock; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + sandbox.stub(events, 'getEvents').returns([]); + agmaConfig = { + options: { + code: 'test', + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('configuration', () => { + it('registers itself with the adapter manager', () => { + const adapter = adapterManager.getAnalyticsAdapter('agma'); + expect(adapter).to.exist; + expect(adapter.gvlid).to.equal(1122); + }); + }); + + describe('getPayload', () => { + it('should use non extended payload with no consent info', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null) + const payload = getPayload([generateUUID()], { + code: 'test', + }); + + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use non extended payload when agma is not in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: false, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use extended payload when agma is in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...extendedKey, 'debug']); + }); + }); + + describe('getTiming', () => { + let originalPerformance; + let originalWindowPerformanceNow; + + beforeEach(() => { + originalPerformance = global.performance; + originalWindowPerformanceNow = window.performance.now; + }); + + afterEach(() => { + global.performance = originalPerformance; + window.performance.now = originalWindowPerformanceNow; + }); + + it('returns TTFB using Timing API V2', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 100, startTime: 50 }]), + now: sinon.stub().returns(150), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 50, elapsedTime: 150 }); + }); + + it('returns TTFB using Timing API V1 when V2 is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: { responseStart: 150, fetchStart: 50 }, + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 100, elapsedTime: 200 }); + }); + + it('returns null when Timing API is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: undefined, + }; + + const result = getTiming(); + + expect(result).to.be.null; + }); + + it('returns ttfb as 0 if calculated value is negative', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 150 }]), + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 200 }); + }); + + it('returns ttfb as 0 if calculated value exceeds performance.now()', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 0 }]), + now: sinon.stub().returns(40), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 40 }); + }); + }); + + describe('getOrtb2Data', () => { + it('returns site and user from options when available', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return {}; + }); + + const ortb2 = { + user: 'user', + site: 'site', + }; + + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal(ortb2); + }); + + it('returns a combination of data from options and pGlobal.readConfig', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return { + ortb2: { + site: { + foo: 'bar', + }, + }, + }; + }); + + const ortb2 = { + user: 'user', + }; + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal({ + site: { + foo: 'bar', + }, + user: 'user', + }); + }); + }); + + describe('Event Payload', () => { + beforeEach(() => { + agmaAnalyticsAdapter.enableAnalytics({ + ...agmaConfig, + }); + server.respondWith('POST', INGEST_URL, [ + 200, + { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + '', + ]); + }); + + afterEach(() => { + agmaAnalyticsAdapter.auctionIds = []; + if (agmaAnalyticsAdapter.timer) { + clearTimeout(agmaAnalyticsAdapter.timer); + } + agmaAnalyticsAdapter.disableAnalytics(); + }); + + it('should only send once per minute', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('1'), + auction, + }); + + clock.tick(200); + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('2'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('3'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('4'), + auction, + }); + + clock.tick(900); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + }); + + it('should send the extended payload with consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1100); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(requestBody.deviceWidth).to.equal(screen.width); + expect(requestBody.deviceHeight).to.equal(screen.height); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should send the non extended payload with no explicit consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + })); + + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should set the trigger Event', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null); + agmaAnalyticsAdapter.disableAnalytics(); + agmaAnalyticsAdapter.enableAnalytics({ + provider: 'agma', + options: { + code: 'test', + triggerEvent: constants.EVENTS.AUCTION_END + }, + }); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + events.emit(constants.EVENTS.AUCTION_END, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.auctionIds).to.have.length(1); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_END); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + }); +}); diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 8b401491ba0..3de348197b2 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -155,9 +155,9 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', + bidId: '2705bfae8ea667', bidder: 'aidem', - bidderRequestId: '15246a574e859f', + bidderRequestId: '1bbb7854dfa0d8', mediaTypes: { banner: { sizes: [ @@ -171,11 +171,7 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ placementId: '13144370' }, src: 'client', - ortb2Imp: { - ext: { - tid: '54a58774-7a41-494e-9aaf-fa7b79164f0c', - }, - }, + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' } ]; @@ -183,9 +179,9 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', + bidId: '2705bfae8ea667', bidder: 'aidem', - bidderRequestId: '15246a574e859f', + bidderRequestId: '1bbb7854dfa0d8', mediaTypes: { video: { minduration: 7, @@ -200,18 +196,23 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ placementId: '13144370' }, src: 'client', - ortb2Imp: { - ext: { - tid: '54a58774-7a41-494e-9aaf-fa7b79164f0c', - } - }, + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' } ]; const VALID_BIDDER_REQUEST = { - auctionId: '6e9b46c3-65a8-46ea-89f4-c5071110c85c', + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', bidderCode: 'aidem', - bidderRequestId: '170ea5d2b1d073', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753' + }, + } + ], refererInfo: { page: 'test-page', domain: 'test-domain', @@ -219,182 +220,68 @@ const VALID_BIDDER_REQUEST = { }, } -// Add mediatype const SERVER_RESPONSE_BANNER = { - body: { - id: 'efa1930a-bc3e-4fd0-8368-08bc40236b4f', - bid: [ - // BANNER - { - 'id': '2e614be960ee1d', - 'impid': '2e614be960ee1d', - 'price': 7.91, - 'mediatype': 'banner', - 'adid': '24277955', - 'adm': 'creativity_banner', - 'adomain': [ - 'aidem.com' - ], - 'iurl': 'http://www.aidem.com', - 'cat': [], - 'cid': '4193561', - 'crid': '24277955', - 'w': 300, - 'h': 250, - 'ext': { - 'dspid': 85, - 'advbrandid': 1246, - 'advbrand': 'AIDEM' - } - }, - ], - cur: 'USD' - }, -} - -const SERVER_RESPONSE_VIDEO = { - body: { - id: 'efa1930a-bc3e-4fd0-8368-08bc40236b4f', - bid: [ - // VIDEO - { - 'id': '2876a29392a47c', - 'impid': '2876a29392a47c', - 'price': 7.93, - 'mediatype': 'video', - 'adid': '24277955', - 'adm': 'https://hermes.aidemsrv.com/vast-tag/cl9mzhhd502uq09l720uegb02?auction_id={{AUCTION_ID}}&cachebuster={{CACHEBUSTER}}', - 'adomain': [ - 'aidem.com' - ], - 'iurl': 'http://www.aidem.com', - 'cat': [], - 'cid': '4193561', - 'crid': '24277955', - 'w': 640, - 'h': 480, - 'ext': { - 'dspid': 85, - 'advbrandid': 1246, - 'advbrand': 'AIDEM' - } - } - ], - cur: 'USD' - }, -} - -const WIN_NOTICE_WEB = { - 'adId': '3a20ee5dc78c1e', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'creativeId': '24277955', - 'cpm': 1, - 'netRevenue': false, - 'adserverTargeting': { - 'hb_bidder': 'aidem', - 'hb_adid': '3a20ee5dc78c1e', - 'hb_pb': '1.00', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner', - 'hb_adomain': 'example.com' - }, - - 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', - 'currency': [ - 'USD' - ], - 'mediaType': 'banner', - 'meta': { - 'advertiserDomains': [ - 'cloudflare.com' - ], - 'ext': {} - }, - 'size': '300x250', - 'params': [ + 'id': '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + 'seatbid': [ { - 'placementId': '13144370', - 'siteId': '23434', - 'publisherId': '7689670753' + 'bid': [ + { + 'id': 'beeswax/aidem', + 'impid': '2705bfae8ea667', + 'price': 0.00875, + 'burl': 'imp_burl', + 'adm': 'creativity_banner', + 'adid': '2:64:162:1001', + 'adomain': [ + 'aidem.com' + ], + 'cid': '64', + 'crid': 'aidem-1001', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 1 + } + ], + 'seat': 'aidemdsp', + 'group': 0 } ], - 'width': 300, - 'height': 250, - 'status': 'rendered', - 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', - 'ttl': 300, - 'requestTimestamp': 1666796241007, - 'responseTimestamp': 1666796241021, - metrics: { - getMetrics() { - return { - - } - } - } + 'cur': 'USD' } -const WIN_NOTICE_APP = { - 'adId': '3a20ee5dc78c1e', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'creativeId': '24277955', - 'cpm': 1, - 'netRevenue': false, - 'adserverTargeting': { - 'hb_bidder': 'aidem', - 'hb_adid': '3a20ee5dc78c1e', - 'hb_pb': '1.00', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner', - 'hb_adomain': 'example.com' - }, - - 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', - 'currency': [ - 'USD' - ], - 'mediaType': 'banner', - 'meta': { - 'advertiserDomains': [ - 'cloudflare.com' - ], - 'ext': { - 'app': { - 'app_bundle': '{{APP_BUNDLE}}', - 'app_id': '{{APP_ID}}', - 'app_name': '{{APP_NAME}}', - 'app_store_url': '{{APP_STORE_URL}}', - 'inventory_source': '{{INVENTORY_SOURCE}}' - }, - 'win_notice_ext': { - 'seatid': '{{SEAT_ID}}' - } - } - }, - 'size': '300x250', - 'params': [ +const SERVER_RESPONSE_VIDEO = { + 'id': '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + 'seatbid': [ { - 'placementId': '13144370', - 'siteId': '23434', - 'publisherId': '7689670753' + 'bid': [ + { + 'id': 'beeswax/aidem', + 'impid': '2705bfae8ea667', + 'price': 0.00875, + 'burl': 'imp_burl', + 'adm': 'creativity_banner', + 'adid': '2:64:162:1001', + 'adomain': [ + 'aidem.com' + ], + 'cid': '64', + 'crid': 'aidem-1001', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 2 + } + ], + 'seat': 'aidemdsp', + 'group': 0 } ], - 'width': 300, - 'height': 250, - 'status': 'rendered', - 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', - 'ttl': 300, - 'requestTimestamp': 1666796241007, - 'responseTimestamp': 1666796241021, - metrics: { - getMetrics() { - return { + 'cur': 'USD' +} - } - } - } +const WIN_NOTICE = { + burl: 'burl' } const ERROR_NOTICE = { @@ -512,109 +399,95 @@ describe('Aidem adapter', () => { const requests = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); expect(requests).to.be.an('object'); expect(requests.method).to.be.a('string') - expect(requests.data).to.be.a('string') + expect(requests.data).to.be.a('object') expect(requests.options).to.be.an('object').that.have.a.property('withCredentials') }); it('should have a well formatted banner payload', () => { - const requests = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.be.a('object').that.has.all.keys( - 'id', 'imp', 'device', 'cur', 'tz', 'regs', 'site', 'environment', 'at' + const {data} = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.be.a('object').that.has.all.keys( + 'id', 'imp', 'regs', 'site', 'environment', 'at', 'test' ) - expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) + expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) - expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'banner', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' + expect(data.imp[0]).to.be.a('object').that.has.all.keys( + 'banner', 'id', 'tagId' ) - expect(payload.imp[0].banner).to.be.a('object').that.has.all.keys( + expect(data.imp[0].banner).to.be.a('object').that.has.all.keys( 'format', 'topframe' ) }); - it('should have a well formatted video payload', () => { - const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.be.a('object').that.has.all.keys( - 'id', 'imp', 'device', 'cur', 'tz', 'regs', 'site', 'environment', 'at' - ) - expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) - - expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'video', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' - ) - expect(payload.imp[0].video).to.be.a('object').that.has.all.keys( - 'format', 'mimes', 'minDuration', 'maxDuration', 'protocols' - ) - }); + if (FEATURES.VIDEO) { + it('should have a well formatted video payload', () => { + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.be.a('object').that.has.all.keys( + 'id', 'imp', 'regs', 'site', 'environment', 'at', 'test' + ) + expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) - it('should have a well formatted bid floor payload if configured', () => { - const validBannerRequests = utils.deepClone(DEFAULT_VALID_BANNER_REQUESTS) - validBannerRequests[0].params.floor = { - value: 1.98, - currency: 'USD' - } - const requests = spec.buildRequests(validBannerRequests, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - const { floor } = payload.imp[0] - expect(floor).to.be.a('object').that.has.all.keys( - 'value', 'currency' - ) - }); + expect(data.imp[0]).to.be.a('object').that.has.all.keys( + 'video', 'id', 'tagId' + ) + expect(data.imp[0].video).to.be.a('object').that.has.all.keys( + 'mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h' + ) + }); + } it('should hav wpar keys in environment object', function () { - const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.have.property('environment') - expect(payload.environment).to.be.a('object').that.have.property('wpar') - expect(payload.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.have.property('environment') + expect(data.environment).to.be.a('object').that.have.property('wpar') + expect(data.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') }); }) describe('interpretResponse', () => { it('should return a valid bid array with a banner bid', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - interpreted.forEach(value => { + const {data} = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_BANNER}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { expect(value).to.be.a('object').that.has.all.keys( - 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'dealId' + 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'burl', 'seatBidId', 'creative_id' ) }) }); - it('should return a valid bid array with a banner bid', () => { - const response = utils.deepClone(SERVER_RESPONSE_VIDEO) - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - interpreted.forEach(value => { - expect(value).to.be.a('object').that.has.all.keys( - 'vastUrl', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'dealId' - ) - }) - }); + if (FEATURES.VIDEO) { + it('should return a valid bid array with a video bid', () => { + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_VIDEO}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'vastUrl', 'vastXml', 'playerHeight', 'playerWidth', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'burl', 'seatBidId', 'creative_id' + ) + }) + }); + } it('should return a valid bid array with netRevenue', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - response.body.bid[0].isNet = true - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - expect(interpreted[0].netRevenue).to.be.true - }); - - it('should return an empty bid array if one of seatbid entry is missing price property', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - delete response.body.bid[0].price - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(0) - }); - - it('should return an empty bid array if one of seatbid entry is missing adm property', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - delete response.body.bid[0].adm - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(0) - }); + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_VIDEO}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + expect(bids[0].netRevenue).to.be.true + }); + + // it('should return an empty bid array if one of seatbid entry is missing price property', () => { + // const response = utils.deepClone(SERVER_RESPONSE_BANNER) + // delete response.body.bid[0].price + // const interpreted = spec.interpretResponse(response) + // expect(interpreted).to.be.a('array').that.has.lengthOf(0) + // }); + + // it('should return an empty bid array if one of seatbid entry is missing adm property', () => { + // const response = utils.deepClone(SERVER_RESPONSE_BANNER) + // delete response.body.bid[0].adm + // const interpreted = spec.interpretResponse(response) + // expect(interpreted).to.be.a('array').that.has.lengthOf(0) + // }); }) describe('onBidWon', () => { @@ -622,34 +495,29 @@ describe('Aidem adapter', () => { expect(spec.onBidWon).to.exist.and.to.be.a('function') }); - it(`should send a valid bid won notice from web environment`, function () { - spec.onBidWon(WIN_NOTICE_WEB); - expect(server.requests.length).to.equal(1); - }); - - it(`should send a valid bid won notice from app environment`, function () { - spec.onBidWon(WIN_NOTICE_APP); + it(`should send a win notice`, function () { + spec.onBidWon(WIN_NOTICE); expect(server.requests.length).to.equal(1); }); }); - describe('onBidderError', () => { - it(`should exists and type function`, function () { - expect(spec.onBidderError).to.exist.and.to.be.a('function') - }); - - it(`should send a valid error notice`, function () { - spec.onBidderError({ bidderRequest: ERROR_NOTICE }) - expect(server.requests.length).to.equal(1); - const body = JSON.parse(server.requests[0].requestBody) - expect(body).to.be.a('object').that.has.all.keys('message', 'auctionId', 'bidderRequestId', 'url', 'metrics') - // const { bids } = JSON.parse(server.requests[0].requestBody) - // expect(bids).to.be.a('array').that.has.lengthOf(1) - // _each(bids, (bid) => { - // expect(bid).to.be.a('object').that.has.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'transactionId', 'metrics') - // }) - }); - }); + // describe('onBidderError', () => { + // it(`should exists and type function`, function () { + // expect(spec.onBidderError).to.exist.and.to.be.a('function') + // }); + // + // it(`should send a valid error notice`, function () { + // spec.onBidderError({ bidderRequest: ERROR_NOTICE }) + // expect(server.requests.length).to.equal(1); + // const body = JSON.parse(server.requests[0].requestBody) + // expect(body).to.be.a('object').that.has.all.keys('message', 'auctionId', 'bidderRequestId', 'url', 'metrics') + // // const { bids } = JSON.parse(server.requests[0].requestBody) + // // expect(bids).to.be.a('array').that.has.lengthOf(1) + // // _each(bids, (bid) => { + // // expect(bid).to.be.a('object').that.has.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'transactionId', 'metrics') + // // }) + // }); + // }); describe('setEndPoints', () => { it(`should exists and type function`, function () { @@ -659,64 +527,35 @@ describe('Aidem adapter', () => { it(`should not modify default endpoints`, function () { const endpoints = setEndPoints() const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(requestURL.host).to.equal('zero.aidemsrv.com') - expect(winNoticeURL.host).to.equal('zero.aidemsrv.com') - expect(timeoutNoticeURL.host).to.equal('zero.aidemsrv.com') - expect(errorNoticeURL.host).to.equal('zero.aidemsrv.com') - - expect(decodeURIComponent(requestURL.pathname)).to.equal('/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/prebidjs/ortb/v2.6/bid/request') }); it(`should not change request endpoint`, function () { const endpoints = setEndPoints('default') const requestURL = new URL(endpoints.request) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/bid/request') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/prebidjs/ortb/v2.6/bid/request') }); it(`should change to local env`, function () { const endpoints = setEndPoints('local') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) expect(requestURL.host).to.equal('127.0.0.1:8787') - expect(winNoticeURL.host).to.equal('127.0.0.1:8787') - expect(timeoutNoticeURL.host).to.equal('127.0.0.1:8787') - expect(errorNoticeURL.host).to.equal('127.0.0.1:8787') }); it(`should add a path prefix`, function () { const endpoints = setEndPoints('local', '/path') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/path/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/path/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/path/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/prebidjs/ortb/v2.6/bid/request') }); it(`should add a path prefix and change request endpoint`, function () { const endpoints = setEndPoints('local', '/path') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/path/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/path/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/path/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/prebidjs/ortb/v2.6/bid/request') }); }); @@ -758,8 +597,7 @@ describe('Aidem adapter', () => { coppa: true }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.coppa_applies).to.equal(true) + expect(data.regs.coppa_applies).to.equal(true) }); it(`should set gdpr to true`, function () { @@ -771,8 +609,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.gdpr_applies).to.equal(true) + expect(data.regs.gdpr_applies).to.equal(true) }); it(`should set usp_consent string`, function () { @@ -789,8 +626,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.us_privacy).to.equal('1YYY') + expect(data.regs.us_privacy).to.equal('1YYY') }); it(`should not set usp_consent string`, function () { @@ -807,8 +643,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.us_privacy).to.undefined + expect(data.regs.us_privacy).to.undefined }); }); }); diff --git a/test/spec/modules/ajaBidAdapter_spec.js b/test/spec/modules/ajaBidAdapter_spec.js index 7cf5698f7d4..dbc72d113f4 100644 --- a/test/spec/modules/ajaBidAdapter_spec.js +++ b/test/spec/modules/ajaBidAdapter_spec.js @@ -62,11 +62,43 @@ describe('AjaAdapter', function () { model: 'SM-G955U', bitness: '64', architecture: '' + }, + ext: { + cdep: 'example_label_1' } } - } + }, + ortb2Imp: { + ext: { + tid: 'cea1eb09-d970-48dc-8585-634d3a7b0544', + gpid: '/1111/homepage#300x250' + } + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + }, + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1, + rid: 'bid-request-2', + name: 'intermediary', + domain: 'intermediary.com' + } + ] + }, } ]; + const serializedSchain = encodeURIComponent('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com!exchange2.com,abcd,1,bid-request-2,intermediary,intermediary.com') const bidderRequest = { refererInfo: { @@ -78,7 +110,7 @@ describe('AjaAdapter', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); - expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&sua=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22Android%22%2C%22version%22%3A%5B%228%22%2C%220%22%2C%220%22%5D%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Not_A%20Brand%22%2C%22version%22%3A%5B%2299%22%2C%220%22%2C%220%22%2C%220%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%2C%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%5D%2C%22mobile%22%3A1%2C%22model%22%3A%22SM-G955U%22%2C%22bitness%22%3A%2264%22%2C%22architecture%22%3A%22%22%7D&'); + expect(requests[0].data).to.equal(`asi=123456&skt=5&gpid=%2F1111%2Fhomepage%23300x250&tid=cea1eb09-d970-48dc-8585-634d3a7b0544&cdep=example_label_1&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&schain=${serializedSchain}&ad_format_ids=2&sua=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22Android%22%2C%22version%22%3A%5B%228%22%2C%220%22%2C%220%22%5D%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Not_A%20Brand%22%2C%22version%22%3A%5B%2299%22%2C%220%22%2C%220%22%2C%220%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%2C%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%5D%2C%22mobile%22%3A1%2C%22model%22%3A%22SM-G955U%22%2C%22bitness%22%3A%2264%22%2C%22architecture%22%3A%22%22%7D&`); }); }); @@ -116,7 +148,7 @@ describe('AjaAdapter', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); - expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&eids=%7B%22eids%22%3A%5B%7B%22source%22%3A%22pubcid.org%22%2C%22uids%22%3A%5B%7B%22id%22%3A%22some-random-id-value%22%2C%22atype%22%3A1%7D%5D%7D%5D%7D&'); + expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&ad_format_ids=2&eids=%7B%22eids%22%3A%5B%7B%22source%22%3A%22pubcid.org%22%2C%22uids%22%3A%5B%7B%22id%22%3A%22some-random-id-value%22%2C%22atype%22%3A1%7D%5D%7D%5D%7D&'); }); }); @@ -173,138 +205,6 @@ describe('AjaAdapter', function () { expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); - it('handles video responses', function () { - let response = { - 'is_ad_return': true, - 'ad': { - 'ad_type': 3, - 'prebid_id': '51ef8751f9aead', - 'price': 12.34, - 'currency': 'JPY', - 'creative_id': '123abc', - 'video': { - 'w': 300, - 'h': 250, - 'vtag': '', - 'purl': 'https://cdn/player', - 'progress': true, - 'loop': false, - 'inread': false, - 'adomain': [ - 'www.example.com' - ] - } - }, - 'syncs': [ - 'https://example.com' - ] - }; - - let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result[0]).to.have.property('vastXml'); - expect(result[0]).to.have.property('renderer'); - expect(result[0]).to.have.property('mediaType', 'video'); - }); - - it('handles native response', function () { - let response = { - 'is_ad_return': true, - 'ad': { - 'ad_type': 2, - 'prebid_id': '51ef8751f9aead', - 'price': 12.34, - 'currency': 'JPY', - 'creative_id': '123abc', - 'native': { - 'template_and_ads': { - 'head': '', - 'body_wrapper': '', - 'body': '', - 'ads': [ - { - 'ad_format_id': 10, - 'assets': { - 'ad_spot_id': '123abc', - 'index': 0, - 'adchoice_url': 'https://aja-kk.co.jp/optout', - 'cta_text': 'cta', - 'img_icon': 'https://example.com/img_icon', - 'img_icon_width': '50', - 'img_icon_height': '50', - 'img_main': 'https://example.com/img_main', - 'img_main_width': '200', - 'img_main_height': '100', - 'lp_link': 'https://example.com/lp?k=v', - 'sponsor': 'sponsor', - 'title': 'ad_title', - 'description': 'ad_desc' - }, - 'imps': [ - 'https://example.com/imp' - ], - 'inviews': [ - 'https://example.com/inview' - ], - 'jstracker': '', - 'disable_trimming': false, - 'adomain': [ - 'www.example.com' - ] - } - ] - } - } - }, - 'syncs': [ - 'https://example.com' - ] - }; - - let expectedResponse = [ - { - 'requestId': '51ef8751f9aead', - 'cpm': 12.34, - 'creativeId': '123abc', - 'dealId': undefined, - 'mediaType': 'native', - 'currency': 'JPY', - 'ttl': 300, - 'netRevenue': true, - 'native': { - 'title': 'ad_title', - 'body': 'ad_desc', - 'cta': 'cta', - 'sponsoredBy': 'sponsor', - 'image': { - 'url': 'https://example.com/img_main', - 'width': 200, - 'height': 100 - }, - 'icon': { - 'url': 'https://example.com/img_icon', - 'width': 50, - 'height': 50 - }, - 'clickUrl': 'https://example.com/lp?k=v', - 'impressionTrackers': [ - 'https://example.com/imp' - ], - 'privacyLink': 'https://aja-kk.co.jp/optout' - }, - 'meta': { - 'advertiserDomains': [ - 'www.example.com' - ] - } - } - ]; - - let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}) - expect(result).to.deep.equal(expectedResponse) - }); - it('handles nobid responses', function () { let response = { 'is_ad_return': false, diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index a396e5b8139..90a9e409e69 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -14,8 +14,7 @@ const REQUEST = { }, 'params': { bidFloor: 0.1, - token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', - pos: 7 + token: 'e64782a4-8e68-4c38-965b-80ccf115d46f' }, 'userIdAsEids': [{ 'source': 'criteo.com', @@ -69,7 +68,7 @@ const BIDDER_VIDEO_RESPONSE = { 'ttl': 200, 'creativeId': 2, 'netRevenue': true, - 'winUrl': 'http://test.com', + 'winUrl': 'http://test.com?price=${AUCTION_PRICE}', 'mediaType': 'video', 'adomain': ['test.com'] }] @@ -96,10 +95,6 @@ describe('alkimiBidAdapter', function () { delete bid.params.token expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) - delete bid.params.bidFloor - expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) delete bid.params expect(spec.isBidRequestValid(bid)).to.equal(false) @@ -109,7 +104,6 @@ describe('alkimiBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [REQUEST] let requestData = { - auctionId: '123', refererInfo: { page: 'http://test.com/path.html' }, @@ -118,7 +112,15 @@ describe('alkimiBidAdapter', function () { vendorData: {}, gdprApplies: true }, - uspConsent: 'uspConsent' + uspConsent: 'uspConsent', + ortb2: { + site: { + keywords: 'test1, test2' + }, + at: 2, + bcat: ['BSW1', 'BSW2'], + wseat: ['16', '165'] + } } const bidderRequest = spec.buildRequests(bidRequests, requestData) @@ -137,13 +139,14 @@ describe('alkimiBidAdapter', function () { 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.requestId).to.not.equal(undefined) expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') expect(bidderRequest.data.schain).to.deep.contains({ ver: '1.0', complete: 1, nodes: [{ asi: 'alkimi-onboarding.com', sid: '00001', hp: 1 }] }) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, sizes: [{width: 300, height: 250}], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode' }) + expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', bidFloor: 0.1, sizes: [{ width: 300, height: 250 }], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode', instl: undefined, exp: undefined, banner: { sizes: [[300, 250]] }, video: undefined }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) + expect(bidderRequest.data.ortb2).to.deep.contains({ at: 2, wseat: ['16', '165'], bcat: ['BSW1', 'BSW2'], site: { keywords: 'test1, test2' }, }) expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) expect(bidderRequest.options.contentType).to.equal('application/json') expect(bidderRequest.url).to.equal(ENDPOINT) @@ -192,9 +195,9 @@ describe('alkimiBidAdapter', function () { 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('winUrl').equal('http://test.com?price=${AUCTION_PRICE}') expect(result[0]).to.have.property('mediaType').equal('video') - expect(result[0]).to.have.property('vastXml').equal('vast') + expect(result[0]).to.have.property('vastUrl').equal('http://test.com?price=800.4') expect(result[0].meta).to.exist.property('advertiserDomains') expect(result[0].meta).to.have.property('advertiserDomains').lengthOf(1) }) diff --git a/test/spec/modules/ampliffyBidAdapter_spec.js b/test/spec/modules/ampliffyBidAdapter_spec.js new file mode 100644 index 00000000000..5b86f692d7e --- /dev/null +++ b/test/spec/modules/ampliffyBidAdapter_spec.js @@ -0,0 +1,453 @@ +import { + parseXML, + isAllowedToBidUp, + spec, + getDefaultParams, + mergeParams, + paramsToQueryString, setCurrentURL +} from 'modules/ampliffyBidAdapter.js'; +import {expect} from 'chai'; +import {BANNER, VIDEO} from 'src/mediaTypes'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('Ampliffy bid adapter Test', 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'); + }); + }); + // Global definitions for all tests + const xmlStr = ` + + + + ]]> + + + + ES + `; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + let companion = xml.getElementsByTagName('Companion')[0]; + let htmlResource = companion.getElementsByTagName('HTMLResource')[0]; + let htmlContent = document.createElement('html'); + htmlContent.innerHTML = htmlResource.textContent; + + describe('Is allowed to bid up', function () { + it('Should return true using a URL that is in domainMap', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://testSports.com?id=131313&text=aaaaa&foo=foo'); + expect(allowedToBidUp).to.be.true; + }) + + it('Should return false using an url that is not in domainMap', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://test.com'); + expect(allowedToBidUp).to.be.false; + }) + + it('Should return false using an url that is excluded.', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://www.no-allowed.com/busqueda/sexo/sexo?test=1#item1'); + expect(allowedToBidUp).to.be.false; + }) + }) + + describe('Helper functions', function () { + it('Should default params not to be null', () => { + const defaultParams = getDefaultParams(); + + expect(defaultParams).not.to.be.null; + }) + it('Should the merge two object params into a new object', () => { + const params1 = { + 'hello': 'world', + 'ampTest': 'this will be replaced' + } + const params2 = { + 'test': 1, + 'ampTest': 'This will be replace the param with the same name in other array' + } + const allParams = mergeParams(params1, params2); + + const paramsComplete = + { + 'hello': 'world', + 'ampTest': 'This will be replace the param with the same name in other array', + 'test': 1, + } + expect(allParams).not.to.be.null; + expect(JSON.stringify(allParams)).to.equal(JSON.stringify(paramsComplete)); + }) + it('Params to QueryString', () => { + const params = { + 'test': 1, + 'ampTest': 'ret', + 'empty': null, + 'quoteMark': '?', + 'test1': undefined + } + const queryString = paramsToQueryString(params); + + expect(queryString).not.to.be.null; + expect(queryString).to.equal('test=1&Test=ret&empty"eMark=%3F'); + }) + }) + + describe('isBidRequestValid', function () { + it('Should return true when required params found', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return false when param format is display but mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'display' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return false when param format is video but mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'video' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return true when param format is video and mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'video' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is display and mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'display' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is all and mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is all and mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return false without placementId param', function () { + const bidRequest = { + bidder: 'ampliffy', + params: {} + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return false without param object', function () { + const bidRequest = { + bidder: 'ampliffy', + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + }); + + describe('Build request function', function () { + const bidderRequest = { + 'bidderCode': 'ampliffy', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'bidderRequestId': '1134bdcbe47f25', + 'bids': [{ + 'bidder': 'ampliffy', + 'params': { + 'placementId': 1235465798, + 'type': 'bidder.', + 'region': 'alan-development.k8s.', + 'adnetwork': 'ampliffy.com', + 'SERVER': 'bidder.ampliffy.com' + }, + 'crumbs': {'pubcid': '29844d69-c4e5-4b00-8602-6dd09815363a'}, + 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video1'}}}, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': 'f85c1b10-bad3-4c3f-a2bb-2c484c405bc9', + 'sizes': [[640, 480]], + 'bidId': '2bc71d9c058842', + 'bidderRequestId': '1134bdcbe47f25', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1644029483655, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'http://localhost:9999/integrationExamples/gpt/hello_world_video.html?pbjs_debug=true', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': ['http://localhost:9999/integrationExamples/gpt/hello_world_video.html?pbjs_debug=true'], + 'canonicalUrl': null + }, + 'start': 1644029483708 + } + const validBidRequests = [ + { + 'bidder': 'ampliffy', + 'params': { + 'placementId': 1235465798, + 'type': 'bidder.', + 'region': 'alan-development.k8s.', + 'adnetwork': 'ampliffy.com', + 'SERVER': 'bidder.ampliffy.com' + }, + 'crumbs': {'pubcid': '29844d69-c4e5-4b00-8602-6dd09815363a'}, + 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video1'}}}, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': 'f85c1b10-bad3-4c3f-a2bb-2c484c405bc9', + 'sizes': [[640, 480]], + 'bidId': '2bc71d9c058842', + 'bidderRequestId': '1134bdcbe47f25', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ]; + it('Should return one or more bid requests', function () { + expect(spec.buildRequests(validBidRequests, bidderRequest).length).to.be.greaterThan(0); + }); + }) + describe('Interpret response', function () { + let bidRequest = { + bidRequest: { + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: '469bb2e2-351f-4d01-b782-cdbca5e3e0ed', + bidId: '2d40b8dcd02ade', + bidRequestsCount: 1, + bidder: 'ampliffy', + bidderRequestId: '128c07edc4680f', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { + pubcid: '29844d69-c4e5-4b00-8602-6dd09815363a' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + ortb2Imp: {ext: {}}, + params: {placementId: 13144370}, + sizes: [ + [300, 250], + [300, 600] + ], + src: 'client', + transactionId: '103b2b58-6ed1-45e9-9486-c942d6042e3' + }, + data: {bidId: '2d40b8dcd02ade'}, + method: 'GET', + url: 'https://test.com', + }; + + it('Should extract a CPM and currency from the xml', () => { + let cpmData = parseXML(xml); + expect(cpmData).to.not.be.a('null'); + expect(cpmData.cpm).to.equal('.23'); + expect(cpmData.currency).to.equal('USD'); + }); + + it('It should return no ads when the CPM is less than zero.', () => { + const xmlStr1 = ` + + + + + + + + +
+
+
+
+ + + ]]> +
+
+ + ES +
+
+
`; + let serverResponse = { + 'body': xmlStr1, + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(0); + }) + + it('It should return no ads when the creative url is not in the xml', () => { + const xmlStr1 = ` + + + + + + + + +
+
+
+
+ + ]]> + + + ES + + + `; + let serverResponse = { + 'body': xmlStr1, + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(0); + }) + it('It should return a banner ad.', () => { + let serverResponse = { + 'body': xmlStr, + } + setCurrentURL('https://www.sports.com'); + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).greaterThan(0); + expect(bidResponses[0].mediaType).to.be.equal(BANNER); + expect(bidResponses[0].ad).not.to.be.null; + }) + it('It should return a video ad.', () => { + let serverResponse = { + 'body': xmlStr, + } + setCurrentURL('https://www.sports.com'); + bidRequest.bidRequest.mediaTypes = { + video: { + sizes: [ + [300, 250], + [300, 600] + ] + } + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).greaterThan(0); + expect(bidResponses[0].mediaType).to.be.equal(VIDEO); + expect(bidResponses[0].vastUrl).not.to.be.null; + }) + }); +}); diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 984c443344d..21fa2e2617c 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/amxBidAdapter.js'; import { createEidsArray } from 'modules/userId/eids.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; +import { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; const sampleRequestId = '82c91e127a9b93e'; @@ -11,7 +12,7 @@ const sampleDisplayCRID = '78827819'; // minimal example vast const sampleVideoAd = (addlImpression) => ` -00:00:15${addlImpression} +00:00:15${addlImpression} `.replace(/\n+/g, ''); const sampleFPD = { @@ -37,7 +38,7 @@ const sampleBidderRequest = { }, gppConsent: { gppString: 'example', - applicableSections: 'example' + applicableSections: 'example', }, auctionId: null, @@ -209,10 +210,12 @@ describe('AmxBidAdapter', () => { describe('getUserSync', () => { it('Will perform an iframe sync even if there is no server response..', () => { const syncs = spec.getUserSyncs({ iframeEnabled: true }); - expect(syncs).to.eql([{ - type: 'iframe', - url: 'https://prebid.a-mo.net/isyn?gdpr_consent=&gdpr=0&us_privacy=&gpp=&gpp_sid=' - }]); + expect(syncs).to.eql([ + { + type: 'iframe', + url: 'https://prebid.a-mo.net/isyn?gdpr_consent=&gdpr=0&us_privacy=&gpp=&gpp_sid=', + }, + ]); }); it('will return valid syncs from a server response', () => { @@ -276,8 +279,13 @@ describe('AmxBidAdapter', () => { }); it('will attach additional referrer info data', () => { - const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); - expect(data.ri.r).to.equal(sampleBidderRequest.refererInfo.topmostLocation); + const { data } = spec.buildRequests( + [sampleBidRequestBase], + sampleBidderRequest + ); + expect(data.ri.r).to.equal( + sampleBidderRequest.refererInfo.topmostLocation + ); expect(data.ri.t).to.equal(sampleBidderRequest.refererInfo.reachedTop); expect(data.ri.l).to.equal(sampleBidderRequest.refererInfo.numIframes); expect(data.ri.s).to.equal(sampleBidderRequest.refererInfo.stack); @@ -315,7 +323,7 @@ describe('AmxBidAdapter', () => { [sampleBidRequestBase], sampleBidderRequest ); - delete data.m; // don't deal with "m" in this test + delete data.m; // don't deal with 'm' in this test expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies); expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString); expect(data.usp).to.equal(sampleBidderRequest.uspConsent); @@ -343,10 +351,8 @@ describe('AmxBidAdapter', () => { }); it('will attach sync configuration', () => { - const request = () => spec.buildRequests( - [sampleBidRequestBase], - sampleBidderRequest - ); + const request = () => + spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); const setConfig = (filterSettings) => config.setConfig({ @@ -355,56 +361,73 @@ describe('AmxBidAdapter', () => { syncDelay: 2300, syncEnabled: true, filterSettings, - } + }, }); const test = (filterSettings) => { setConfig(filterSettings); return request().data.sync; - } + }; const base = { d: 2300, l: 2, e: true }; - const tests = [[ - undefined, - { ...base, t: 0 } - ], [{ - image: { - bidders: '*', - filter: 'include' - }, - iframe: { - bidders: '*', - filter: 'include' - } - }, { ...base, t: 3 }], [{ - image: { - bidders: ['amx'], - }, - iframe: { - bidders: '*', - filter: 'include' - } - }, { ...base, t: 3 }], [{ - image: { - bidders: ['other'], - }, - iframe: { - bidders: '*' - } - }, { ...base, t: 2 }], [{ - image: { - bidders: ['amx'] - }, - iframe: { - bidders: ['amx'], - filter: 'exclude' - } - }, { ...base, t: 1 }]] + const tests = [ + [undefined, { ...base, t: 0 }], + [ + { + image: { + bidders: '*', + filter: 'include', + }, + iframe: { + bidders: '*', + filter: 'include', + }, + }, + { ...base, t: 3 }, + ], + [ + { + image: { + bidders: ['amx'], + }, + iframe: { + bidders: '*', + filter: 'include', + }, + }, + { ...base, t: 3 }, + ], + [ + { + image: { + bidders: ['other'], + }, + iframe: { + bidders: '*', + }, + }, + { ...base, t: 2 }, + ], + [ + { + image: { + bidders: ['amx'], + }, + iframe: { + bidders: ['amx'], + filter: 'exclude', + }, + }, + { ...base, t: 1 }, + ], + ]; for (let i = 0, l = tests.length; i < l; i++) { const [result, expected] = tests[i]; - expect(test(result), `input: ${JSON.stringify(result)}`).to.deep.equal(expected); + expect(test(result), `input: ${JSON.stringify(result)}`).to.deep.equal( + expected + ); } }); @@ -497,7 +520,15 @@ describe('AmxBidAdapter', () => { it('can build a video request', () => { const { data } = spec.buildRequests( - [{ ...sampleBidRequestVideo, params: { ...sampleBidRequestVideo.params, adUnitId: 'custom-auid' } }], + [ + { + ...sampleBidRequestVideo, + params: { + ...sampleBidRequestVideo.params, + adUnitId: 'custom-auid', + }, + }, + ], sampleBidderRequest ); expect(Object.keys(data.m).length).to.equal(1); @@ -659,15 +690,49 @@ describe('AmxBidAdapter', () => { }); it('will log an event for timeout', () => { - spec.onTimeout({ - bidder: 'example', - bidId: 'test-bid-id', - adUnitCode: 'div-gpt-ad', - timeout: 300, - auctionId: utils.getUniqueIdentifierStr(), + // this will use sendBeacon.. + spec.onTimeout([ + { + bidder: 'example', + bidId: 'test-bid-id', + adUnitCode: 'div-gpt-ad', + ortb2: { + site: { + ref: 'https://example.com', + }, + }, + params: { + tagId: 'tag-id', + }, + timeout: 300, + auctionId: utils.getUniqueIdentifierStr(), + }, + ]); + + const [request] = server.requests; + request.respond(204, {'Content-Type': 'text/html'}, null); + expect(request.url).to.equal('https://1x1.a-mo.net/e'); + + if (typeof Request !== 'undefined' && 'keepalive' in Request.prototype) { + expect(request.fetch.request.keepalive).to.equal(true); + } + + const {c: common, e: events} = JSON.parse(request.requestBody) + expect(common).to.deep.equal({ + V: '$prebid.version$', + vg: '$$PREBID_GLOBAL$$', + U: null, + re: 'https://example.com', }); - expect(firedPixels.length).to.equal(1); - expect(firedPixels[0]).to.match(/\/hbx\/g_pbto/); + + expect(events.length).to.equal(1); + const [event] = events; + expect(event.n).to.equal('g_pbto') + expect(event.A).to.equal('example'); + expect(event.mid).to.equal('tag-id'); + expect(event.cn).to.equal(300); + expect(event.bid).to.equal('test-bid-id'); + expect(event.a).to.equal('div-gpt-ad'); }); it('will log an event for prebid win', () => { diff --git a/test/spec/modules/anyclipBidAdapter_spec.js b/test/spec/modules/anyclipBidAdapter_spec.js new file mode 100644 index 00000000000..3de36f9fe06 --- /dev/null +++ b/test/spec/modules/anyclipBidAdapter_spec.js @@ -0,0 +1,160 @@ +import { expect } from 'chai'; +import { spec } from 'modules/anyclipBidAdapter.js'; + +describe('anyclipBidAdapter', function () { + afterEach(function () { + global._anyclip = undefined; + }); + + let bid; + + function mockBidRequest() { + return { + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90], + [468, 60] + ] + } + }, + bidder: 'anyclip', + params: { + publisherId: '12345', + supplyTagId: '-mptNo0BycUG4oCDgGrU' + } + }; + }; + + describe('isBidRequestValid', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('should return true if all required fields are present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('should return false if bidder does not correspond', function () { + bid.bidder = 'abc'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if params object is missing', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if publisherId is missing from params', function () { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if supplyTagId is missing from params', function () { + delete bid.params.supplyTagId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if mediaTypes is missing', function () { + delete bid.mediaTypes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if banner is missing from mediaTypes ', function () { + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if sizes is missing from banner object', function () { + delete bid.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if sizes is not an array', function () { + bid.mediaTypes.banner.sizes = 'test'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return false if sizes is an empty array', function () { + bid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let bidderRequest = { + refererInfo: { + page: 'http://example.com', + domain: 'example.com', + }, + timeout: 3000 + }; + + this.beforeEach(function () { + bid = mockBidRequest(); + Object.assign(bid, { + adUnitCode: '1', + transactionId: '123', + sizes: bid.mediaTypes.banner.sizes + }); + }); + + it('when pubtag is not available, return undefined', function () { + expect(spec.buildRequests([bid], bidderRequest)).to.undefined; + }); + it('when pubtag is available, creates a ServerRequest object with method, URL and data', function() { + global._anyclip = { + PubTag: function() {}, + pubTag: { + requestBids: function() {} + } + }; + expect(spec.buildRequests([bid], bidderRequest)).to.exist; + }); + }); + + describe('interpretResponse', function() { + it('should return an empty array when parsing a no bid response', function () { + const response = {}; + const request = {}; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(0); + }); + it('should return bids array', function() { + const response = {}; + const request = { + bidRequest: { + bidId: 'test-bidId', + transactionId: '123' + } + }; + + global._anyclip = { + PubTag: function() {}, + pubTag: { + getBids: function(transactionId) { + return { + adServer: { + bid: { + ad: 'test-ad', + creativeId: 'test-crId', + meta: { + advertiserDomains: ['anyclip.com'] + }, + width: 300, + height: 250, + ttl: 300 + } + }, + cpm: 1.23, + } + } + } + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].netRevenue).to.false; + expect(bids[0].meta.advertiserDomains[0]).to.equal('anyclip.com'); + }); + }); +}); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 13ef31a68d4..cc86a8a0aaa 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -343,7 +343,7 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].hb_source).to.deep.equal(1); }); - it('should include ORTB video values when video params were not set', function () { + it('should include ORTB video values when matching video params were not all set', function () { let bidRequest = deepClone(bidRequests[0]); bidRequest.params = { placementId: '1234235', @@ -377,6 +377,61 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) }); + it('should include ORTB video values when video params is empty - case 1', function () { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'outstream', + placement: 3, + mimes: ['video/mp4'], + skip: 0, + minduration: 5, + api: [1, 5, 6], + playbackmethod: [2, 4] + } + }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].video).to.deep.equal({ + minduration: 5, + playback_method: 2, + skippable: false, + context: 4 + }); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) + }); + + it('should include ORTB video values when video params is empty - case 2', function () { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'outstream', + plcmt: 2, + startdelay: -1, + mimes: ['video/mp4'], + skip: 1, + minduration: 5, + api: [1, 5, 6], + playbackmethod: [2, 4] + } + }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].video).to.deep.equal({ + minduration: 5, + playback_method: 2, + skippable: true, + context: 9 + }); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) + }); + it('should add video property when adUnit includes a renderer', function () { const videoData = { mediaTypes: { @@ -1093,7 +1148,18 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].use_pmt_rule).to.equal(true); }); - it('should add gpid to the request', function () { + it('should add preferred gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { gpid: testGpid } }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) + }); + + it('should add backup gpid to the request', function () { let testGpid = '/12345/my-gpt-tag-0'; let bidRequest = deepClone(bidRequests[0]); bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; @@ -1193,6 +1259,46 @@ describe('AppNexusAdapter', function () { expect(payload.privacy.gpp_sid).to.deep.equal([7]); }); + it('should add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa', function () { + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': { + 'regs': { + 'ext': { + 'dsa': { + 'dsarequired': 1, + 'pubrender': 0, + 'datatopub': 1, + 'transparency': [{ + 'domain': 'good-domain', + 'dsaparams': [1, 2] + }, { + 'domain': 'bad-setup', + 'dsaparams': ['1', 3] + }] + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa.dsarequired).to.equal(1); + expect(payload.dsa.pubrender).to.equal(0); + expect(payload.dsa.datatopub).to.equal(1); + expect(payload.dsa.transparency).to.deep.equal([{ + 'domain': 'good-domain', + 'dsaparams': [1, 2] + }]); + }); + it('supports sending hybrid mobile app parameters', function () { let appRequest = Object.assign({}, bidRequests[0], @@ -1564,6 +1670,15 @@ describe('AppNexusAdapter', function () { 'viewability': { 'config': '' }, + 'dsa': { + 'behalf': 'test-behalf', + 'paid': 'test-paid', + 'transparency': [{ + 'domain': 'good-domain', + 'params': [1, 2, 3] + }], + 'adrender': 1 + }, 'rtb': { 'banner': { 'content': '', @@ -1612,6 +1727,15 @@ describe('AppNexusAdapter', function () { 'nodes': [{ 'bsid': '958' }] + }, + 'dsa': { + 'behalf': 'test-behalf', + 'paid': 'test-paid', + 'transparency': [{ + 'domain': 'good-domain', + 'params': [1, 2, 3] + }], + 'adrender': 1 } } } diff --git a/test/spec/modules/asoBidAdapter_spec.js b/test/spec/modules/asoBidAdapter_spec.js index 88016d1902c..e317a8828e7 100644 --- a/test/spec/modules/asoBidAdapter_spec.js +++ b/test/spec/modules/asoBidAdapter_spec.js @@ -1,30 +1,30 @@ import {expect} from 'chai'; import {spec} from 'modules/asoBidAdapter.js'; -import {parseUrl} from 'src/utils.js'; -import {BANNER, VIDEO} from 'src/mediaTypes.js'; +import {BANNER, VIDEO, NATIVE} from 'src/mediaTypes.js'; +import {OUTSTREAM} from 'src/video.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd'; +import {parseUrl} from '../../../src/utils'; + +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; describe('Adserver.Online bidding adapter', function () { const bannerRequest = { bidder: 'aso', params: { - zone: 1, - attr: { - keywords: ['a', 'b'], - tags: ['t1', 't2'] - } + zone: 1 }, adUnitCode: 'adunit-banner', + bidId: 'bid-banner', mediaTypes: { - banner: { + [BANNER]: { sizes: [ [300, 250], [240, 400], ] } }, - bidId: 'bidid1', - bidderRequestId: 'bidreq1', - auctionId: 'auctionid1', userIdAsEids: [{ source: 'src1', uids: [ @@ -38,32 +38,67 @@ describe('Adserver.Online bidding adapter', function () { const videoRequest = { bidder: 'aso', params: { - zone: 2, - video: { - api: [2], - maxduration: 30 - } + zone: 2 }, + adUnitCode: 'adunit-video', + bidId: 'bid-video', mediaTypes: { - video: { - context: 'outstream', + [VIDEO]: { + context: OUTSTREAM, playerSize: [[640, 480]], protocols: [1, 2], mimes: ['video/mp4'], } + } + }; + + const nativeOrtbRequest = { + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + }, + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 600 + } + }] + }; + + const nativeRequest = { + bidder: 'aso', + params: { + zone: 3 + }, + adUnitCode: 'adunit-native', + bidId: 'bid-native', + mediaTypes: { + [NATIVE]: { + ortb: { + ...nativeOrtbRequest + } + } }, - adUnitCode: 'adunit-video', - bidId: 'bidid12', - bidderRequestId: 'bidreq2', - auctionId: 'auctionid12' + nativeOrtbRequest }; const bidderRequest = { refererInfo: { - numIframes: 0, + page: 'https://example.com/page.html', + topmostLocation: 'https://example.com/page.html', reachedTop: true, - page: 'https://example.com', - domain: 'example.com' + numIframes: 1, + stack: [ + 'https://example.com/page.html', + 'https://example.com/iframe1.html' + ] } }; @@ -81,6 +116,14 @@ describe('Adserver.Online bidding adapter', function () { } }; + const gdprNotApplies = { + gdprApplies: false, + consentString: '', + vendorData: { + purpose: {} + } + }; + const uspConsent = 'usp_consent'; describe('isBidRequestValid', function () { @@ -110,81 +153,121 @@ describe('Adserver.Online bidding adapter', function () { }); }); - describe('buildRequests', function () { - it('creates a valid banner request', function () { - bannerRequest.getFloor = () => ({ currency: 'USD', floor: 0.5 }); + describe('requests builder', function () { + it('should add bid floor', function () { + const bidRequest = Object.assign({}, bannerRequest); + + bidRequest.getFloor = () => { + return { + currency: 'USD', + floor: 0.5 + } + }; + + const payload = spec.buildRequests([bidRequest], bidderRequest)[0].data; + + expect(payload.imp[0].bidfloor).to.equal(0.5); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); + }); + it('endpoint is valid', function () { const requests = spec.buildRequests([bannerRequest], bidderRequest); expect(requests).to.have.lengthOf(1); const request = requests[0]; expect(request).to.exist; expect(request.method).to.equal('POST'); - const parsedRequestUrl = parseUrl(request.url); - expect(parsedRequestUrl.hostname).to.equal('srv.aso1.net'); - expect(parsedRequestUrl.pathname).to.equal('/prebid/bidder'); + const parsedUrl = parseUrl(request.url); + expect(parsedUrl.hostname).to.equal('srv.aso1.net'); + expect(parsedUrl.pathname).to.equal('/prebid/bidder'); - const query = parsedRequestUrl.search; + const query = parsedUrl.search; expect(query.pbjs).to.contain('$prebid.version$'); expect(query.zid).to.equal('1'); + }); + it('creates a valid banner request', function () { + const requests = spec.buildRequests([bannerRequest], syncAddFPDToBidderRequest(bidderRequest)); + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request).to.exist; expect(request.data).to.exist; const payload = request.data; - expect(payload.site).to.not.equal(null); - expect(payload.site.ref).to.equal(''); - expect(payload.site.page).to.equal('https://example.com'); + expect(payload.site).to.exist; + expect(payload.site.page).to.equal('https://example.com/page.html'); - expect(payload.device).to.not.equal(null); + expect(payload.device).to.exist; expect(payload.device.w).to.equal(window.innerWidth); expect(payload.device.h).to.equal(window.innerHeight); expect(payload.imp).to.have.lengthOf(1); expect(payload.imp[0].tagid).to.equal('adunit-banner'); - expect(payload.imp[0].banner).to.not.equal(null); - expect(payload.imp[0].banner.w).to.equal(300); - expect(payload.imp[0].banner.h).to.equal(250); - expect(payload.imp[0].bidfloor).to.equal(0.5); - expect(payload.imp[0].bidfloorcur).to.equal('USD'); + expect(payload.imp[0].banner).to.not.null; + expect(payload.imp[0].banner.format).to.have.lengthOf(2); + expect(payload.imp[0].banner.format[0].w).to.equal(300); + expect(payload.imp[0].banner.format[0].h).to.equal(250); + expect(payload.imp[0].banner.format[1].w).to.equal(240); + expect(payload.imp[0].banner.format[1].h).to.equal(400); }); - it('creates a valid video request', function () { - const requests = spec.buildRequests([videoRequest], bidderRequest); - expect(requests).to.have.lengthOf(1); - const request = requests[0]; + if (FEATURES.VIDEO) { + it('creates a valid video request', function () { + const requests = spec.buildRequests([videoRequest], syncAddFPDToBidderRequest(bidderRequest)); + expect(requests).to.have.lengthOf(1); + const request = requests[0]; - expect(request).to.exist; - expect(request.method).to.equal('POST'); - const parsedRequestUrl = parseUrl(request.url); - expect(parsedRequestUrl.hostname).to.equal('srv.aso1.net'); - expect(parsedRequestUrl.pathname).to.equal('/prebid/bidder'); + expect(request).to.exist; + expect(request.data).to.not.be.empty; - const query = parsedRequestUrl.search; - expect(query.pbjs).to.contain('$prebid.version$'); - expect(query.zid).to.equal('2'); + const payload = request.data; - expect(request.data).to.not.be.empty; + expect(payload.site).to.exist; + expect(payload.site.page).to.equal('https://example.com/page.html'); - const payload = request.data; + expect(payload.device).to.exist; + expect(payload.device.w).to.equal(window.innerWidth); + expect(payload.device.h).to.equal(window.innerHeight); - expect(payload.site).to.not.equal(null); - expect(payload.site.ref).to.equal(''); - expect(payload.site.page).to.equal('https://example.com'); + expect(payload.imp).to.have.lengthOf(1); - expect(payload.device).to.not.equal(null); - expect(payload.device.w).to.equal(window.innerWidth); - expect(payload.device.h).to.equal(window.innerHeight); + expect(payload.imp[0].tagid).to.equal('adunit-video'); + expect(payload.imp[0].video).to.exist; - expect(payload.imp).to.have.lengthOf(1); + expect(payload.imp[0].video.w).to.equal(640); + expect(payload.imp[0].video.h).to.equal(480); + expect(payload.imp[0].banner).to.not.exist; + }); + } - expect(payload.imp[0].tagid).to.equal('adunit-video'); - expect(payload.imp[0].video).to.not.equal(null); - expect(payload.imp[0].video.w).to.equal(640); - expect(payload.imp[0].video.h).to.equal(480); - expect(payload.imp[0].banner).to.be.undefined; - }); + if (FEATURES.NATIVE) { + it('creates a valid native request', function () { + const requests = spec.buildRequests([nativeRequest], syncAddFPDToBidderRequest(bidderRequest)); + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request).to.exist; + expect(request.data).to.not.be.empty; + + const payload = request.data; + + expect(payload.site).to.exist; + expect(payload.site.page).to.equal('https://example.com/page.html'); + + expect(payload.device).to.exist; + expect(payload.device.w).to.equal(window.innerWidth); + expect(payload.device.h).to.equal(window.innerHeight); + + expect(payload.imp).to.have.lengthOf(1); + + expect(payload.imp[0].tagid).to.equal('adunit-native'); + expect(payload.imp[0].native).to.exist; + expect(payload.imp[0].native.request).to.exist; + }); + } }); describe('GDPR/USP compliance', function () { @@ -192,7 +275,7 @@ describe('Adserver.Online bidding adapter', function () { bidderRequest.gdprConsent = gdprConsent; bidderRequest.uspConsent = uspConsent; - const requests = spec.buildRequests([bannerRequest], bidderRequest); + const requests = spec.buildRequests([bannerRequest], syncAddFPDToBidderRequest(bidderRequest)); expect(requests).to.have.lengthOf(1); const request = requests[0]; @@ -209,7 +292,7 @@ describe('Adserver.Online bidding adapter', function () { bidderRequest.gdprConsent = null; bidderRequest.uspConsent = null; - const requests = spec.buildRequests([bannerRequest], bidderRequest); + const requests = spec.buildRequests([bannerRequest], syncAddFPDToBidderRequest(bidderRequest)); expect(requests).to.have.lengthOf(1); const request = requests[0]; @@ -226,18 +309,21 @@ describe('Adserver.Online bidding adapter', function () { describe('response handler', function () { const bannerResponse = { body: { - id: 'auctionid1', - bidid: 'bidid1', seatbid: [{ bid: [ { - impid: 'impid1', + impid: 'bid-banner', price: 0.3, crid: 321, adm: '', w: 300, h: 250, adomain: ['example.com'], + ext: { + prebid: { + type: 'banner' + } + } } ] }], @@ -255,18 +341,48 @@ describe('Adserver.Online bidding adapter', function () { const videoResponse = { body: { - id: 'auctionid2', - bidid: 'bidid2', seatbid: [{ bid: [ { - impid: 'impid2', + impid: 'bid-video', price: 0.5, crid: 123, adm: '', adomain: ['example.com'], w: 640, h: 480, + ext: { + prebid: { + type: 'video' + } + } + } + ] + }], + cur: 'USD' + }, + }; + + const nativeResponse = { + body: { + seatbid: [{ + bid: [ + { + impid: 'bid-native', + price: 0.5, + crid: 123, + adm: JSON.stringify({ + assets: [ + {id: 0, title: {text: 'Title'}}, + {id: 1, img: {type: 3, url: 'https://img'}}, + ], + }), + adomain: ['example.com'], + ext: { + prebid: { + type: 'native' + } + } } ] }], @@ -275,47 +391,59 @@ describe('Adserver.Online bidding adapter', function () { }; it('handles banner responses', function () { - bannerRequest.bidRequest = { - mediaType: BANNER - }; - const result = spec.interpretResponse(bannerResponse, bannerRequest); - - expect(result).to.have.lengthOf(1); - - expect(result[0]).to.exist; - expect(result[0].width).to.equal(300); - expect(result[0].height).to.equal(250); - expect(result[0].mediaType).to.equal(BANNER); - expect(result[0].creativeId).to.equal(321); - expect(result[0].cpm).to.be.within(0.1, 0.5); - expect(result[0].ad).to.equal(''); - expect(result[0].currency).to.equal('USD'); - expect(result[0].netRevenue).to.equal(true); - expect(result[0].ttl).to.equal(300); - expect(result[0].dealId).to.not.exist; - expect(result[0].meta.advertiserDomains[0]).to.equal('example.com'); + const request = spec.buildRequests([bannerRequest], bidderRequest)[0]; + const bids = spec.interpretResponse(bannerResponse, request); + + expect(bids).to.have.lengthOf(1); + + expect(bids[0]).to.exist; + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].mediaType).to.equal(BANNER); + expect(bids[0].creativeId).to.equal(321); + expect(bids[0].cpm).to.be.within(0.1, 0.5); + expect(bids[0].ad).to.equal(''); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].dealId).to.not.exist; + expect(bids[0].meta.advertiserDomains[0]).to.equal('example.com'); }); - it('handles video responses', function () { - const request = { - bidRequest: videoRequest - }; - request.bidRequest.mediaType = VIDEO; - - const result = spec.interpretResponse(videoResponse, request); - expect(result).to.have.lengthOf(1); - - expect(result[0].width).to.equal(640); - expect(result[0].height).to.equal(480); - expect(result[0].mediaType).to.equal(VIDEO); - expect(result[0].creativeId).to.equal(123); - expect(result[0].cpm).to.equal(0.5); - expect(result[0].vastXml).to.equal(''); - expect(result[0].renderer).to.be.a('object'); - expect(result[0].currency).to.equal('USD'); - expect(result[0].netRevenue).to.equal(true); - expect(result[0].ttl).to.equal(300); - }); + if (FEATURES.VIDEO) { + it('handles video responses', function () { + const request = spec.buildRequests([videoRequest], bidderRequest)[0]; + const bids = spec.interpretResponse(videoResponse, request); + expect(bids).to.have.lengthOf(1); + + expect(bids[0].width).to.equal(640); + expect(bids[0].height).to.equal(480); + expect(bids[0].mediaType).to.equal(VIDEO); + expect(bids[0].creativeId).to.equal(123); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].vastXml).to.equal(''); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ttl).to.equal(300); + }); + } + + if (FEATURES.NATIVE) { + it('handles native responses', function () { + const request = spec.buildRequests([nativeRequest], bidderRequest)[0]; + const bids = spec.interpretResponse(nativeResponse, request); + expect(bids).to.have.lengthOf(1); + + expect(bids[0].mediaType).to.equal(NATIVE); + expect(bids[0].creativeId).to.equal(123); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ttl).to.equal(300); + + expect(bids[0].native.ortb.assets).to.have.lengthOf(2); + }); + } it('handles empty responses', function () { const response = []; @@ -331,11 +459,27 @@ describe('Adserver.Online bidding adapter', function () { }; it('should return iframe sync option', function () { - expect(spec.getUserSyncs(syncOptions, [bannerResponse], gdprConsent, uspConsent)[0].type).to.equal('iframe'); - expect(spec.getUserSyncs(syncOptions, [bannerResponse], gdprConsent, uspConsent)[0].url).to.equal( - 'sync_url?gdpr=1&consents_str=consentString&consents=1%2C2&us_privacy=usp_consent&' + const syncs = spec.getUserSyncs(syncOptions, [bannerResponse], gdprConsent, uspConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal( + 'sync_url?gdpr=1&consents_str=consentString&consents=1%2C2&us_privacy=usp_consent' ); }); + + it('should return iframe sync option - gdpr not applies', function () { + const syncs = spec.getUserSyncs(syncOptions, [bannerResponse], gdprNotApplies, uspConsent); + expect(syncs).to.have.lengthOf(1); + + expect(syncs[0].url).to.equal( + 'sync_url?us_privacy=usp_consent' + ); + }); + + it('should return no sync option', function () { + const syncs = spec.getUserSyncs(syncOptions, [videoResponse], gdprNotApplies, uspConsent); + expect(syncs).to.have.lengthOf(0); + }); }); }); }); diff --git a/test/spec/modules/asteriobidAnalyticsAdapter_spec.js b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9be6c1dedac --- /dev/null +++ b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js @@ -0,0 +1,151 @@ +import asteriobidAnalytics, {storage} from 'modules/asteriobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {expectEvents} from '../../helpers/analytics.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('AsterioBid Analytics Adapter', function () { + let bidWonEvent = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': '1ebb82ec35375e', + 'mediaType': 'banner', + 'cpm': 0.5, + 'requestId': '1582271863760569973', + 'creative_id': '96846035', + 'creativeId': '96846035', + 'ttl': 60, + 'currency': 'USD', + 'netRevenue': true, + 'auctionId': '9c7b70b9-b6ab-4439-9e71-b7b382797c18', + 'responseTimestamp': 1537521629657, + 'requestTimestamp': 1537521629331, + 'bidder': 'appnexus', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 326, + 'size': '300x250', + 'status': 'rendered', + 'eventType': 'bidWon', + 'ad': 'some ad', + 'adUrl': 'ad url' + }; + + describe('AsterioBid Analytic tests', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + asteriobidAnalytics.disableAnalytics(); + events.getEvents.restore(); + }); + + it('support custom endpoint', function () { + let custom_url = 'custom url'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + url: custom_url, + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expect(asteriobidAnalytics.getOptions().url).to.equal(custom_url); + }); + + it('bid won event', function() { + let bundleId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: bundleId + } + }); + + events.emit(constants.EVENTS.BID_WON, bidWonEvent); + asteriobidAnalytics.flush(); + + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://endpt.asteriobid.com/endpoint'); + expect(server.requests[0].requestBody.substring(0, 2)).to.equal('1:'); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + expect(pmEvents.pageViewId).to.exist; + expect(pmEvents.bundleId).to.equal(bundleId); + expect(pmEvents.ver).to.equal(1); + expect(pmEvents.events.length).to.equal(1); + expect(pmEvents.events[0].eventType).to.equal('bidWon'); + expect(pmEvents.events[0].ad).to.be.undefined; + expect(pmEvents.events[0].adUrl).to.be.undefined; + }); + + it('track event without errors', function () { + sinon.spy(asteriobidAnalytics, 'track'); + + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expectEvents().to.beTrackedBy(asteriobidAnalytics.track); + }); + }); + + describe('build utm tag data', function () { + let getDataFromLocalStorageStub; + this.timeout(4000) + beforeEach(function () { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs('pm_utm_source').returns('utm_source'); + getDataFromLocalStorageStub.withArgs('pm_utm_medium').returns('utm_medium'); + getDataFromLocalStorageStub.withArgs('pm_utm_campaign').returns('utm_camp'); + getDataFromLocalStorageStub.withArgs('pm_utm_term').returns(''); + getDataFromLocalStorageStub.withArgs('pm_utm_content').returns(''); + }); + afterEach(function () { + getDataFromLocalStorageStub.restore(); + asteriobidAnalytics.disableAnalytics() + }); + it('should build utm data from local storage', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.utmTags.utm_source).to.equal('utm_source'); + expect(pmEvents.utmTags.utm_medium).to.equal('utm_medium'); + expect(pmEvents.utmTags.utm_campaign).to.equal('utm_camp'); + expect(pmEvents.utmTags.utm_term).to.equal(''); + expect(pmEvents.utmTags.utm_content).to.equal(''); + }); + }); + + describe('build page info', function () { + afterEach(function () { + asteriobidAnalytics.disableAnalytics() + }); + it('should build page info', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + 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/automatadAnalyticsAdapter_spec.js b/test/spec/modules/automatadAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..a7dd28a8dc0 --- /dev/null +++ b/test/spec/modules/automatadAnalyticsAdapter_spec.js @@ -0,0 +1,620 @@ +import * as events from 'src/events'; +import * as utils from 'src/utils.js'; + +import spec, {self as exports} from 'modules/automatadAnalyticsAdapter.js'; + +import CONSTANTS from 'src/constants.json'; +import { expect } from 'chai'; + +const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} +} + +const { + AUCTION_DEBUG, + BID_REQUESTED, + BID_REJECTED, + AUCTION_INIT, + BIDDER_DONE, + BID_RESPONSE, + BID_TIMEOUT, + BID_WON, + NO_BID +} = CONSTANTS.EVENTS + +const CONFIG_WITH_DEBUG = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + }, + includeEvents: [AUCTION_DEBUG, AUCTION_INIT, BIDDER_DONE, BID_RESPONSE, BID_TIMEOUT, NO_BID, BID_WON, BID_REQUESTED, BID_REJECTED] +} + +describe('Automatad Analytics Adapter', () => { + var sandbox, clock; + + describe('Adapter Setup Configuration', () => { + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'logMessage') + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('Should log error and return false if nothing is passed as the param in the enable analytics call', () => { + spec.enableAnalytics() + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if object type is not passed as the param in the enable analytics call', () => { + spec.enableAnalytics('hello world') + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if options is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter' + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + siteID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should successfully configure the adapter and set global log debug messages flag to false', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421', + logDebug: false + } + }); + expect(utils.logError.called).to.equal(false) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + it('Should successfully configure the adapter and set global log debug messages flag to true', () => { + sandbox.stub(exports, 'initializeQueue').callsFake(() => {}); + sandbox.stub(exports, 'addGPTHandlers').callsFake(() => {}); + const config = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '410', + logDebug: true + } + } + + spec.enableAnalytics(config) + expect(utils.logError.called).to.equal(false) + expect(exports.initializeQueue.called).to.equal(true) + expect(exports.addGPTHandlers.called).to.equal(true) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + }); + + describe('Behaviour of the adapter when the sdk has loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + + global.window.atmtdAnalytics = obj + exports.qBeingUsed = false + exports.qTraversalComplete = undefined + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + const handlers = global.window.atmtdAnalytics + Object.keys(handlers).forEach((handler) => global.window.atmtdAnalytics[handler].reset()) + global.window.atmtdAnalytics = undefined; + spec.disableAnalytics(); + exports.qBeingUsed = false + exports.qTraversalComplete = undefined + }) + + it('Should call the auctionInitHandler when the auction init event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(global.window.atmtdAnalytics.auctionInitHandler.called).to.equal(true) + }); + + it('Should call the bidRequested when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(global.window.atmtdAnalytics.bidRequestedHandler.called).to.equal(true) + }); + + it('Should call the bidRejected when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(global.window.atmtdAnalytics.bidRejectedHandler.called).to.equal(true) + }); + + it('Should call the bidResponseHandler when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(global.window.atmtdAnalytics.bidResponseHandler.called).to.equal(true) + }); + + it('Should call the bidderDoneHandler when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(global.window.atmtdAnalytics.bidderDoneHandler.called).to.equal(true) + }); + + it('Should call the bidWonHandler when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(global.window.atmtdAnalytics.bidWonHandler.called).to.equal(true) + }); + + it('Should call the noBidHandler when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(global.window.atmtdAnalytics.noBidHandler.called).to.equal(true) + }); + + it('Should call the bidTimeoutHandler when the bidTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.called).to.equal(true) + }); + + it('Should call the auctionDebugHandler when the auctionDebug event is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(global.window.atmtdAnalytics.auctionDebugHandler.called).to.equal(true) + }); + }); + + describe('Behaviour of the adapter when the SDK has not loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + + global.window.atmtdAnalytics = undefined + exports.__atmtdAnalyticsQueue.length = 0 + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + spec.disableAnalytics(); + }) + + it('Should push to the que when the auctionInit event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_INIT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_INIT) + }); + + it('Should push to the que when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_RESPONSE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_RESPONSE) + }); + + it('Should push to the que when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REQUESTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REQUESTED) + }); + + it('Should push to the que when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REJECTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REJECTED) + }); + + it('Should push to the que when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BIDDER_DONE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BIDDER_DONE) + }); + + it('Should push to the que when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_WON) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_WON) + }); + + it('Should push to the que when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(NO_BID) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(NO_BID) + }); + + it('Should push to the que when the auctionDebug is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_DEBUG) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_DEBUG) + }); + + it('Should push to the que when the bidderTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_TIMEOUT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_TIMEOUT) + }); + }); + + describe('Behaviour of the adapter when the SDK has loaded midway', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + + global.window.atmtdAnalytics = undefined + + exports.qBeingUsed = undefined + exports.qTraversalComplete = undefined + exports.queuePointer = 0 + exports.retryCount = 0 + exports.__atmtdAnalyticsQueue.length = 0 + + clock = sandbox.useFakeTimers(); + + sandbox.spy(exports.__atmtdAnalyticsQueue, 'push') + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + spec.disableAnalytics(); + }) + + it('Should push to the que when the auctionInit event is fired and push to the que even after SDK has loaded after auctionInit event', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_RESPONSE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_RESPONSE) + expect(exports.qBeingUsed).to.equal(true) + expect(exports.qTraversalComplete).to.equal(undefined) + global.window.atmtdAnalytics = obj + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue.push.calledTwice).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[1]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[1][0]).to.equal(BID_RESPONSE) + expect(exports.__atmtdAnalyticsQueue[1][1].type).to.equal(BID_RESPONSE) + expect(exports.qBeingUsed).to.equal(true) + expect(exports.qTraversalComplete).to.equal(undefined) + }); + + it('Should push to the que when the auctionInit event is fired and push to the analytics adapter handler after the que is processed', () => { + expect(exports.qBeingUsed).to.equal(undefined) + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + global.window.atmtdAnalytics = {...obj} + const handlers = global.window.atmtdAnalytics + Object.keys(handlers).forEach((handler) => global.window.atmtdAnalytics[handler].reset()) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_INIT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_INIT) + expect(exports.qBeingUsed).to.equal(true) + expect(exports.qTraversalComplete).to.equal(undefined) + expect(global.window.atmtdAnalytics.auctionInitHandler.callCount).to.equal(0) + clock.tick(2000) + expect(exports.qBeingUsed).to.equal(true) + expect(exports.qTraversalComplete).to.equal(undefined) + events.emit(NO_BID, {type: NO_BID}) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue.push.calledTwice).to.equal(true) + clock.tick(1500) + expect(exports.qBeingUsed).to.equal(false) + expect(exports.qTraversalComplete).to.equal(true) + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue.push.calledTwice).to.equal(true) + expect(exports.__atmtdAnalyticsQueue.push.calledThrice).to.equal(false) + expect(global.window.atmtdAnalytics.auctionInitHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.noBidHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidResponseHandler.calledOnce).to.equal(true) + }); + }); + + describe('Process Events from Que when SDK still has not loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + global.window.atmtdAnalytics = undefined + + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + exports.queuePointer = 0; + exports.retryCount = 0; + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + sandbox.spy(exports, 'processEvents') + + clock = sandbox.useFakeTimers(); + exports.__atmtdAnalyticsQueue.length = 0 + }); + afterEach(() => { + sandbox.restore(); + exports.queuePointer = 0; + exports.retryCount = 0; + spec.disableAnalytics(); + }) + + it('Should retry processing auctionInit in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [[AUCTION_INIT, {type: AUCTION_INIT}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing slotRenderEnded in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['slotRenderEnded', {type: 'slotRenderEnded'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing impressionViewable in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['impressionViewable', {type: 'impressionViewable'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + }); + + describe('Process Events from Que when SDK has loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + sandbox = sinon.createSandbox(); + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + impressionViewableHandler: (args) => {}, + slotRenderEndedGPTHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj; + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + exports.retryCount = 0; + exports.queuePointer = 0; + exports.__atmtdAnalyticsQueue = [ + [AUCTION_INIT, {type: AUCTION_INIT}], + [BID_RESPONSE, {type: BID_RESPONSE}], + [BID_REQUESTED, {type: BID_REQUESTED}], + [BID_REJECTED, {type: BID_REJECTED}], + [NO_BID, {type: NO_BID}], + [BID_WON, {type: BID_WON}], + [BIDDER_DONE, {type: BIDDER_DONE}], + [AUCTION_DEBUG, {type: AUCTION_DEBUG}], + [BID_TIMEOUT, {type: BID_TIMEOUT}], + ['slotRenderEnded', {type: 'slotRenderEnded'}], + ['impressionViewable', {type: 'impressionViewable'}] + ] + }); + afterEach(() => { + sandbox.restore(); + }) + after(() => { + spec.disableAnalytics(); + }) + + it('Should make calls to appropriate SDK event handlers', () => { + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.retryCount).to.equal(0) + expect(exports.prettyLog.callCount).to.equal(1) + expect(exports.queuePointer).to.equal(exports.__atmtdAnalyticsQueue.length) + expect(global.window.atmtdAnalytics.auctionInitHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidResponseHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRejectedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRequestedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.noBidHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidWonHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.auctionDebugHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderDoneHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.slotRenderEndedGPTHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.impressionViewableHandler.calledOnce).to.equal(true) + }) + }); + + describe('Prettylog fn tests', () => { + beforeEach(() => { + sandbox = sinon.createSandbox() + sandbox.spy(utils, 'logInfo') + sandbox.spy(utils, 'logError') + exports.isLoggingEnabled = true + }) + + afterEach(() => { + sandbox.restore() + }) + + it('Should call logMessage once in normal mode', () => { + exports.prettyLog('status', 'Hello world') + expect(utils.logInfo.callCount).to.equal(1) + }) + + it('Should call logMessage twice in group mode and have the cb called', () => { + const spy = sandbox.spy() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(spy.called).to.equal(true) + }) + + it('Should call logMessage twice in group mode and have the cb which throws an error', () => { + const spy = sandbox.stub().throws() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(utils.logError.called).to.equal(true) + }) + }); +}); diff --git a/test/spec/modules/axisBidAdapter_spec.js b/test/spec/modules/axisBidAdapter_spec.js new file mode 100644 index 00000000000..083f05f5c0a --- /dev/null +++ b/test/spec/modules/axisBidAdapter_spec.js @@ -0,0 +1,414 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/axisBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'axis' + +describe('AxisBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + integration: '000000', + token: '000000' + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + ortb2: { + site: { + cat: ['IAB24'] + } + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, 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://prebid.axis-marketplace.com/pbjs'); + }); + + it('Returns general data 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', + 'iabCat', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + 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.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.iabCat).to.have.lengthOf(1); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.integration).to.be.a('string'); + expect(placement.token).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + expect(placement.pos).to.be.within(0, 7); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + expect(placement.pos).to.be.within(0, 7); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, 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 = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, 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([], bidderRequest); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + width: 300, + height: 250, + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta', 'width', 'height'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&ccpa=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/azerionedgeRtdProvider_spec.js b/test/spec/modules/azerionedgeRtdProvider_spec.js new file mode 100644 index 00000000000..f08aaebdf55 --- /dev/null +++ b/test/spec/modules/azerionedgeRtdProvider_spec.js @@ -0,0 +1,183 @@ +import { config } from 'src/config.js'; +import * as azerionedgeRTD from 'modules/azerionedgeRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; + +describe('Azerion Edge RTD submodule', function () { + const STORAGE_KEY = 'ht-pa-v1-a'; + const USER_AUDIENCES = [ + { id: '1', visits: 123 }, + { id: '2', visits: 456 }, + ]; + + const key = 'publisher123'; + const bidders = ['appnexus', 'improvedigital']; + const process = { key: 'value' }; + const dataProvider = { name: 'azerionedge', waitForIt: true }; + + let reqBidsConfigObj; + let storageStub; + + beforeEach(function () { + config.resetConfig(); + reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + window.azerionPublisherAudiences = sinon.spy(); + storageStub = sinon.stub(azerionedgeRTD.storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + delete window.azerionPublisherAudiences; + storageStub.restore(); + }); + + describe('initialisation', function () { + let returned; + + beforeEach(function () { + returned = azerionedgeRTD.azerionedgeSubmodule.init(dataProvider); + }); + + it('should return true', function () { + expect(returned).to.equal(true); + }); + + it('should load external script', function () { + expect(loadExternalScript.called).to.be.true; + }); + + it('should load external script with default versioned url', function () { + const expected = 'https://edge.hyth.io/js/v1/azerion-edge.min.js'; + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + }); + + it('should call azerionPublisherAudiencesStub with empty configuration', function () { + expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal({}); + }); + + describe('with key', function () { + beforeEach(function () { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + returned = azerionedgeRTD.azerionedgeSubmodule.init({ + ...dataProvider, + params: { key }, + }); + }); + + it('should return true', function () { + expect(returned).to.equal(true); + }); + + it('should load external script with publisher id url', function () { + const expected = `https://edge.hyth.io/js/v1/${key}/azerion-edge.min.js`; + expect(loadExternalScript.args[0][0]).to.deep.equal(expected); + }); + }); + + describe('with process configuration', function () { + beforeEach(function () { + window.azerionPublisherAudiences.resetHistory(); + loadExternalScript.resetHistory(); + returned = azerionedgeRTD.azerionedgeSubmodule.init({ + ...dataProvider, + params: { process }, + }); + }); + + it('should return true', function () { + expect(returned).to.equal(true); + }); + + it('should call azerionPublisherAudiencesStub with process configuration', function () { + expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal( + process + ); + }); + }); + }); + + describe('gets audiences', function () { + let callbackStub; + + beforeEach(function () { + callbackStub = sinon.mock(); + }); + + describe('with empty storage', function () { + beforeEach(function () { + azerionedgeRTD.azerionedgeSubmodule.getBidRequestData( + reqBidsConfigObj, + callbackStub, + dataProvider + ); + }); + + it('does not run apply audiences to bidders', function () { + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({}); + }); + + it('calls callback anyway', function () { + expect(callbackStub.called).to.be.true; + }); + }); + + describe('with populate storage', function () { + beforeEach(function () { + storageStub + .withArgs(STORAGE_KEY) + .returns(JSON.stringify(USER_AUDIENCES)); + azerionedgeRTD.azerionedgeSubmodule.getBidRequestData( + reqBidsConfigObj, + callbackStub, + dataProvider + ); + }); + + it('does apply audiences to bidder', function () { + const segments = + reqBidsConfigObj.ortb2Fragments.bidder['improvedigital'].user.data[0] + .segment; + expect(segments).to.deep.equal([{ id: '1' }, { id: '2' }]); + }); + + it('calls callback always', function () { + expect(callbackStub.called).to.be.true; + }); + }); + }); + + describe('sets audiences in bidder', function () { + const audiences = USER_AUDIENCES.map(({ id }) => id); + const expected = { + user: { + data: [ + { + ext: { segtax: 4 }, + name: 'azerionedge', + segment: [{ id: '1' }, { id: '2' }], + }, + ], + }, + }; + + it('for improvedigital by default', function () { + azerionedgeRTD.setAudiencesToBidders( + reqBidsConfigObj, + dataProvider, + audiences + ); + expect( + reqBidsConfigObj.ortb2Fragments.bidder['improvedigital'] + ).to.deep.equal(expected); + }); + + bidders.forEach((bidder) => { + it(`for ${bidder}`, function () { + const config = { ...dataProvider, params: { bidders } }; + azerionedgeRTD.setAudiencesToBidders(reqBidsConfigObj, config, audiences); + expect(reqBidsConfigObj.ortb2Fragments.bidder[bidder]).to.deep.equal( + expected + ); + }); + }); + }); +}); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 4e30b822a61..c0994985aae 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; -import { config } from 'src/config.js'; -import { parseUrl, deepAccess } from 'src/utils.js'; +import { parseUrl } from 'src/utils.js'; describe('BeachfrontAdapter', function () { let bidRequests; @@ -296,6 +295,23 @@ describe('BeachfrontAdapter', function () { expect(data.user.ext.consent).to.equal(consentString); }); + it('must add GPP consent data to the request', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + const gppString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const applicableSections = [1, 2, 3]; + const bidderRequest = { + gppConsent: { + gppString, + applicableSections + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.regs.gpp).to.equal(gppString); + expect(data.regs.gpp_sid).to.deep.equal(applicableSections); + }); + it('must add schain data to the request', () => { const schain = { ver: '1.0', @@ -517,6 +533,23 @@ describe('BeachfrontAdapter', function () { expect(data.gdprConsent).to.equal(consentString); }); + it('must add GPP consent data to the request', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + const gppString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const applicableSections = [1, 2, 3]; + const bidderRequest = { + gppConsent: { + gppString, + applicableSections + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.gpp).to.equal(gppString); + expect(data.gppSid).to.deep.equal(applicableSections); + }); + it('must add schain data to the request', () => { const schain = { ver: '1.0', diff --git a/test/spec/modules/beopBidAdapter_spec.js b/test/spec/modules/beopBidAdapter_spec.js index c77e304e539..663d622e505 100644 --- a/test/spec/modules/beopBidAdapter_spec.js +++ b/test/spec/modules/beopBidAdapter_spec.js @@ -312,4 +312,22 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.kwds).to.include('keywords'); }) }) + + describe('Ensure eids are get', function() { + let bidRequests = []; + afterEach(function () { + bidRequests = []; + }); + + it(`should get eids from bid`, function () { + let bid = Object.assign({}, validBid); + bid.userIdAsEids = [{source: 'provider.com', uids: [{id: 'someid', atype: 1, ext: {whatever: true}}]}]; + bidRequests.push(bid); + + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + expect(payload.eids).to.exist; + expect(payload.eids[0].source).to.equal('provider.com'); + }); + }) }); diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index a822d86f852..2d2e51abbe1 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -245,18 +245,31 @@ describe('#bidViewability', function() { let logWinningBidNotFoundSpy; let callBidViewableBidderSpy; let winningBidsArray; + let callBidBillableBidderSpy; + let adUnits = [ + { + 'code': 'abc123', + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + } + ]; beforeEach(function() { sandbox = sinon.sandbox.create(); triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); + callBidBillableBidderSpy = sandbox.spy(adapterManager, ['callBidBillableBidder']); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ getAllWinningBids: function (number) { return winningBidsArray; - } + }, + adUnits }); }); @@ -293,5 +306,23 @@ describe('#bidViewability', function() { // CONSTANTS.EVENTS.BID_VIEWABLE is NOT triggered expect(eventsEmitSpy.callCount).to.equal(0); }); + + it('should call the callBidBillableBidder function if the viewable bid is associated with an ad unit with deferBilling set to true', function() { + let moduleConfig = {}; + const deferredBillingAdUnit = { + 'code': '/harshad/Jan/2021/', + 'deferBilling': true, + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + }; + adUnits.push(deferredBillingAdUnit); + winningBidsArray.push(PBJS_WINNING_BID); + bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); + expect(callBidBillableBidderSpy.callCount).to.equal(1); + sinon.assert.calledWith(callBidBillableBidderSpy, PBJS_WINNING_BID); + }); }); }); diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js index f80051b0a50..f8e66caf657 100644 --- a/test/spec/modules/bizzclickBidAdapter_spec.js +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -1,6 +1,102 @@ import { expect } from 'chai'; -import { spec } from 'modules/bizzclickBidAdapter.js'; -import {config} from 'src/config.js'; +import { spec } from 'modules/bizzclickBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from '../../../src/config.js'; +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; + +// load modules that register ORTB processors +import 'src/prebid.js'; +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; + +const SIMPLE_BID_REQUEST = { + bidder: 'bizzclick', + params: { + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', + }, + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1499748733608-0', + transactionId: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + bidId: '33e9500b21129f', + bidderRequestId: '2772c1e566670b', + auctionId: '192721e36a0239', + sizes: [[300, 250], [160, 600]], + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', + }, +} + +const BANNER_BID_REQUEST = { + bidder: 'bizzclick', + params: { + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + code: 'banner_example', + timeout: 1000, +} + +const VIDEO_BID_REQUEST = { + placementCode: '/DfpAccount1/slotVideo', + bidId: 'test-bid-id-2', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + bidder: 'bizzclick', + params: { + accountId: '123', + sourceId: '123', + host: 'USE', + }, + adUnitCode: '/adunit-code/test-path', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, +} const NATIVE_BID_REQUEST = { code: 'native_example', @@ -34,386 +130,179 @@ const NATIVE_BID_REQUEST = { }, bidder: 'bizzclick', params: { - placementId: 'hash', - accountId: 'accountId' - }, - timeout: 1000 - -}; - -const BANNER_BID_REQUEST = { - code: 'banner_example', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - schain: { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'example.com', - sid: '164', - hp: 1 - } - ] - }, - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', timeout: 1000, - gdprConsent: { - consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', - gdprApplies: 1, - }, uspConsent: 'uspConsent' -} +}; -const bidRequest = { +const bidderRequest = { 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 - ] + page: 'https://publisher.com/home', + ref: 'https://referrer' } - }, - - 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 gdprConsent = { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', +} -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 () { + 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('BizzclickAdapter', function() { - describe('with COPPA', function() { - beforeEach(function() { + describe('with user privacy regulations', function () { + it('should send the Coppa "required" flag set to "1" in the request', function () { sinon.stub(config, 'getConfig') .withArgs('coppa') .returns(true); - }); - afterEach(function() { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(serverRequest.data.regs.coppa).to.equal(1); config.getConfig.restore(); }); - it('should send the Coppa "required" flag set to "1" in the request', function () { - let serverRequest = spec.buildRequests([BANNER_BID_REQUEST]); - expect(serverRequest.data[0].regs.coppa).to.equal(1); + it('should send the GDPR Consent data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({ ...bidderRequest, gdprConsent })); + expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); }); - }); - 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); + it('should send the CCPA data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); }); }); - 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'); + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(true); }); - it('Returns empty data if no valid requests are passed', function () { - let serverRequest = spec.buildRequests([]); - expect(serverRequest).to.be.an('array').that.is.empty; + it('should return false when accountID/sourceId is missing', function () { + let localbid = Object.assign({}, BANNER_BID_REQUEST); + delete localbid.params.accountId; + delete localbid.params.sourceId; + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(false); }); }); - 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; + describe('build request', function () { + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], syncAddFPDToBidderRequest(bidderRequest)); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); }); - it('sends bid request to our endpoint via POST', function () { + it('should return a valid bid request object', function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request).to.not.equal('array'); + expect(request.data).to.be.an('object'); expect(request.method).to.equal('POST'); + expect(request.url).to.not.equal(''); + expect(request.url).to.not.equal(undefined); + expect(request.url).to.not.equal(null); + + expect(request.data.site).to.have.property('page'); + expect(request.data.site).to.have.property('domain'); + expect(request.data).to.have.property('id'); + expect(request.data).to.have.property('imp'); + expect(request.data).to.have.property('device'); }); - it('check consent and ccpa string is set properly', function() { - expect(request.data[0].regs.ext.gdpr).to.equal(1); - expect(request.data[0].user.ext.consent).to.equal(BANNER_BID_REQUEST.gdprConsent.consentString); - expect(request.data[0].regs.ext.us_privacy).to.equal(BANNER_BID_REQUEST.uspConsent); - }) - - it('check schain is set properly', function() { - expect(request.data[0].source.ext.schain.complete).to.equal(1); - expect(request.data[0].source.ext.schain.ver).to.equal('1.0'); - }) - - 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('should return a valid bid BANNER request object', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); + expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); }); - }); - 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; - }); + if (FEATURES.VIDEO) { + it('should return a valid bid VIDEO request object', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.w).to.be.an('number'); + expect(request.data.imp[0].video.h).to.be.an('number'); + }); + } - it('sends bid request to our endpoint via POST', function () { - expect(request.method).to.equal('POST'); + it('should return a valid bid NATIVE request object', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0]).to.be.an('object'); }); + }) - 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 () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [{ + 'bidId': '28ffdk2B952532', + 'bidder': 'bizzclick', + 'userId': { + 'freepassId': { + 'userIp': '172.21.0.1', + 'userId': '123', + 'commonId': 'commonIdValue' + } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' + } + }]; + bidderRequest = {}; }); - }); - describe('interpretResponse', function () { - it('Empty response must return empty array', function() { + it('Empty response must return empty array', function () { const emptyResponse = null; - let response = spec.interpretResponse(emptyResponse); + let response = spec.interpretResponse(emptyResponse, BANNER_BID_REQUEST); 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, - - meta: {advertiserDomains: BANNER_BID_RESPONSE.seatbid[0].bid[0].adomain}, - 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', 'meta', '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.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - 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, - meta: {advertiserDomains: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adomain}, - 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', 'meta', '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.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - 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', - meta: {advertiserDomains: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adomain}, - native: {clickUrl: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adm.native.link.url} - } - - let nativeResponses = spec.interpretResponse(nativeResponse); - - expect(nativeResponses).to.be.an('array').that.is.not.empty; - let dataItem = nativeResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - 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); - }); + const serverResponse = { + body: { + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'impid': '28ffdk2B952532', + 'price': 97, + 'adm': '', + 'w': 300, + 'h': 250, + 'crid': 'creative0' + }] + }] + } + }; + it('should interpret server response', function () { + const bidRequest = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(serverResponse, bidRequest); + expect(bids).to.be.an('array'); + const bid = bids[0]; + expect(bid).to.be.an('object'); + expect(bid.currency).to.equal('USD'); + expect(bid.cpm).to.equal(97); + expect(bid.ad).to.equal(ad) + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('creative0'); + }); + }) }); -}) +}); diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index 8e96bd76940..3db97a17d88 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -1,5 +1,16 @@ -import { expect } from 'chai' -import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME } from 'modules/bliinkBidAdapter.js' +import { expect } from 'chai'; +import { + spec, + buildBid, + BLIINK_ENDPOINT_ENGINE, + getMetaList, + BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME, + getEffectiveConnectionType, + getUserIds, + getDomLoadingDuration, + GVL_ID, +} from 'modules/bliinkBidAdapter.js'; +import { config } from 'src/config.js'; /** * @description Mockup bidRequest @@ -20,6 +31,9 @@ import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_CO * crumbs: {pubcid: string}, * ortb2Imp: {ext: {data: {pbadslot: string}}}}} */ + +const connectionType = getEffectiveConnectionType(); +const domLoadingDuration = getDomLoadingDuration().toString(); const getConfigBid = (placement) => { return { adUnitCode: '/19968336/test', @@ -31,31 +45,33 @@ const getConfigBid = (placement) => { bidderRequestsCount: 1, bidderWinsCount: 0, crumbs: { - pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d' + pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d', }, sizes: [[300, 250]], mediaTypes: { banner: { - sizes: [ - [300, 250] - ] - } + sizes: [[300, 250]], + }, }, ortb2Imp: { ext: { data: { - pbadslot: '/19968336/test' - } - } + pbadslot: '/19968336/test', + }, + }, }, + domLoadingDuration, + ect: connectionType, params: { placement: placement, - tagId: '14f30eca-85d2-11e8-9eed-0242ac120007' + tagId: '14f30eca-85d2-11e8-9eed-0242ac120007', + videoUrl: 'https://www.example.com/advideo.mp4', + imageUrl: 'https://www.example.com/adimage.jpg', }, src: 'client', - transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf' - } -} + transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf', + }; +}; const getConfigBannerBid = () => { return { creative: { @@ -76,14 +92,13 @@ const getConfigBannerBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; const getConfigVideoBid = () => { return { creative: { video: { - content: - '', + content: '', height: 250, width: 300, }, @@ -99,8 +114,8 @@ const getConfigVideoBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; /** * @description Mockup response from engine.bliink.io/xxxx @@ -119,7 +134,7 @@ const getConfigVideoBid = () => { * } * } * } -* } + * } */ const getConfigCreative = () => { return { @@ -132,8 +147,8 @@ const getConfigCreative = () => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; const getConfigCreativeVideo = (isNoVast) => { return { @@ -147,8 +162,8 @@ const getConfigCreativeVideo = (isNoVast) => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; /** * @description Mockup BuildRequest function @@ -166,19 +181,19 @@ const getConfigBuildRequest = (placement) => { reachedTop: true, page: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', }, - } + }; if (!placement) { - return buildRequest + return buildRequest; } return Object.assign(buildRequest, { params: { bids: [getConfigBid(placement)], - placement: placement + placement: placement, }, - }) -} + }); +}; /** * @description Mockup response from API @@ -189,8 +204,8 @@ const getConfigInterpretResponse = (noAd = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } return { @@ -198,11 +213,12 @@ const getConfigInterpretResponse = (noAd = false) => { ...getConfigCreative(), mode: 'ad', transactionId: '2def0c5b2a7f6e', - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8' + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8', }, headers: {}, - } -} + }; +}; /** * @description Mockup response from API for RTB creative @@ -213,8 +229,8 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } const validVast = ` @@ -229,41 +245,43 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { - ` + `; const invalidVast = ` - ` + `; return { - body: { bids: [ - { - 'creative': { - 'video': { - 'content': isInvalidVast ? invalidVast : validVast, - 'height': 250, - 'width': 300 + body: { + bids: [ + { + creative: { + video: { + content: isInvalidVast ? invalidVast : validVast, + height: 250, + width: 300, + }, + media_type: 'video', + creativeId: 0, }, - 'media_type': 'video', - 'creativeId': 0, - }, - 'price': 0, - 'id': '8121', - 'token': 'token', - 'mode': 'rtb', - 'extras': { - 'deal_id': '34567ertyaza', - 'transaction_id': '2def0c5b2a7f6e' + price: 0, + id: '8121', + token: 'token', + mode: 'rtb', + extras: { + deal_id: '34567ertyaza', + transaction_id: '2def0c5b2a7f6e', + }, + currency: 'EUR', }, - 'currency': 'EUR' - } - ], - userSyncs: []} - } -} + ], + userSyncs: [], + }, + }; +}; /** * @@ -279,9 +297,9 @@ const testsGetMetaList = [ { title: 'Should return empty array if there are no parameters', args: { - fn: getMetaList() + fn: getMetaList(), }, - want: [] + want: [], }, { title: 'Should return list of metas with name associated', @@ -313,18 +331,63 @@ const testsGetMetaList = [ key: 'property', value: `'article:${'test'}'`, }, - ] - } -] + ], + }, +]; -describe('BLIINK Adapter getMetaList', function() { +describe('BLIINK Adapter getMetaList', function () { for (const test of testsGetMetaList) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); +const GetUserIds = [ + { + title: 'Should return undefined if there are no parameters', + args: { + fn: getUserIds(), + }, + want: undefined, + }, + { + title: 'Should return eids if exists', + args: { + fn: getUserIds([{ userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ] }]), + }, + want: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ], + }, +]; + +describe('BLIINK Adapter getUserIds', function () { + for (const test of GetUserIds) { + it(test.title, () => { + const res = test.args.fn; + expect(res).to.eql(test.want); + }); + } +}); /** * @description Array of tests used in describe function below @@ -349,127 +412,142 @@ const testsIsBidRequestValid = [ { title: 'isBidRequestValid format not valid', args: { - fn: spec.isBidRequestValid({}) + fn: spec.isBidRequestValid({}), }, want: false, }, { title: 'isBidRequestValid does not receive any bid', args: { - fn: spec.isBidRequestValid() + fn: spec.isBidRequestValid(), }, want: false, }, { title: 'isBidRequestValid Receive a valid bid', args: { - fn: spec.isBidRequestValid(getConfigBid('banner')) + fn: spec.isBidRequestValid(getConfigBid('banner')), }, want: true, - } -] + }, +]; -describe('BLIINK Adapter isBidRequestValid', function() { +describe('BLIINK Adapter isBidRequestValid', function () { for (const test of testsIsBidRequestValid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); -const vastXml = getConfigInterpretResponseRTB().body.bids[0].creative.video.content +const vastXml = + getConfigInterpretResponseRTB().body.bids[0].creative.video.content; const testsInterpretResponse = [ { title: 'Should construct bid for video instream', args: { - fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)) + fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)), }, - want: [{ - cpm: 0, - currency: 'EUR', - height: 250, - width: 300, - creativeId: '34567ertyaza', - mediaType: 'video', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(vastXml.replace(/\\"/g, '"')) - }] + want: [ + { + cpm: 0, + currency: 'EUR', + height: 250, + width: 300, + creativeId: '34567ertyaza', + mediaType: 'video', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + vastXml, + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(vastXml.replace(/\\"/g, '"')), + }, + ], }, { title: 'ServerResponse with message: invalid tag, return empty array', args: { - fn: spec.interpretResponse(getConfigInterpretResponse(true)) + fn: spec.interpretResponse(getConfigInterpretResponse(true)), }, - want: [] + want: [], }, { title: 'ServerResponse with mediaType banner', args: { - fn: spec.interpretResponse({body: {bids: [getConfigBannerBid()]}}), + fn: spec.interpretResponse({ body: { bids: [getConfigBannerBid()] } }), }, - want: [{ - ad: '', - cpm: 1, - creativeId: '34567erty', - currency: 'EUR', - height: 250, - mediaType: 'banner', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - width: 300 - }] + want: [ + { + ad: '', + cpm: 1, + creativeId: '34567erty', + currency: 'EUR', + height: 250, + mediaType: 'banner', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + width: 300, + }, + ], }, { title: 'ServerResponse with unhandled mediaType, return empty array', args: { - fn: spec.interpretResponse({body: {bids: [{...getConfigBannerBid(), - creative: { - unknown: { - adm: '', - height: 250, - width: 300, - }, - media_type: 'unknown', - creativeId: 125, - requestId: '2def0c5b2a7f6e', - }}]}}), + fn: spec.interpretResponse({ + body: { + bids: [ + { + ...getConfigBannerBid(), + creative: { + unknown: { + adm: '', + height: 250, + width: 300, + }, + media_type: 'unknown', + creativeId: 125, + requestId: '2def0c5b2a7f6e', + }, + }, + ], + }, + }), }, - want: [] + want: [], }, -] +]; -describe('BLIINK Adapter interpretResponse', function() { +describe('BLIINK Adapter interpretResponse', function () { for (const test of testsInterpretResponse) { it(test.title, () => { - const res = test.args.fn + const res = test.args.fn; if (res) { - expect(res).to.eql(test.want) + expect(res).to.eql(test.want); } - }) + }); } -}) +}); /** * @description Array of tests used in describe function below * @type {[ * {args: * {fn: { - * cpm: number, - * netRevenue: boolean, - * ad, requestId, - * meta: {mediaType}, - * width: number, - * currency: string, - * ttl: number, - * creativeId: number, - * height: number + * cpm: number, + * netRevenue: boolean, + * ad, requestId, + * meta: {mediaType}, + * width: number, + * currency: string, + * ttl: number, + * creativeId: number, + * height: number * } * }, want, title: string}]} */ @@ -478,21 +556,26 @@ const testsBuildBid = [ { title: 'Should return null if no bid passed in parameters', args: { - fn: buildBid() + fn: buildBid(), }, - want: null + want: null, }, { title: 'Input data must respect the output model', args: { - fn: buildBid({ id: 1, test: '123' }, { id: 2, test: '345' }, false, false) + fn: buildBid( + { id: 1, test: '123' }, + { id: 2, test: '345' }, + false, + false + ), }, - want: null + want: null, }, { title: 'input data respect the output model for video', args: { - fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()) + fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()), }, want: { requestId: getConfigBid('video').bidId, @@ -504,25 +587,31 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'use default height width output model for video', args: { - fn: buildBid({...getConfigVideoBid('video'), - creative: { - video: { - content: - '', - height: null, - width: null, + fn: buildBid( + { + ...getConfigVideoBid('video'), + creative: { + video: { + content: '', + height: null, + width: null, + }, + media_type: 'video', + creativeId: getConfigVideoBid().extras.deal_id, + requestId: '2def0c5b2a7f6e', }, - media_type: 'video', - creativeId: getConfigVideoBid().extras.deal_id, - requestId: '2def0c5b2a7f6e', - }}, getConfigCreativeVideo()) + }, + getConfigCreativeVideo() + ), }, want: { requestId: getConfigBid('video').bidId, @@ -534,14 +623,16 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'input data respect the output model for banner', args: { - fn: buildBid(getConfigBannerBid()) + fn: buildBid(getConfigBannerBid()), }, want: { requestId: getConfigBid('banner').bidId, @@ -554,18 +645,18 @@ const testsBuildBid = [ ad: getConfigBannerBid().creative.banner.adm, ttl: 300, netRevenue: true, - } - } -] + }, + }, +]; -describe('BLIINK Adapter buildBid', function() { +describe('BLIINK Adapter buildBid', function () { for (const test of testsBuildBid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); /** * @description Array of tests used in describe function below @@ -575,28 +666,33 @@ const testsBuildRequests = [ { title: 'Should not build request, no bidder request exist', args: { - fn: spec.buildRequests() + fn: spec.buildRequests(), }, - want: null + want: null, }, { title: 'Should build request if bidderRequest exist', args: { - fn: spec.buildRequests([], getConfigBuildRequest('banner')) + fn: spec.buildRequests([], getConfigBuildRequest('banner')), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, keywords: '', pageDescription: '', pageTitle: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -605,35 +701,43 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, }, { title: 'Should build request width GDPR configuration', args: { - fn: spec.buildRequests([], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -642,52 +746,115 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, + }, + { + title: 'Should build request width uspConsent if exists', + args: { + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + uspConsent: 'uspConsent', + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + uspConsent: 'uspConsent', + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, }, { title: 'Should build request width schain if exists', args: { - fn: spec.buildRequests([{schain: { - ver: '1.0', - complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] - }}], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [ + { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], + }, + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', schain: { ver: '1.0', complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], }, tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -696,107 +863,313 @@ const testsBuildRequests = [ }, ], }, - ] - } - } - } -] + ], + }, + }, + }, + { + title: 'Should build request with eids if exists', + args: { + fn: spec.buildRequests( + [ + { + userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + eids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, + }, +]; -describe('BLIINK Adapter buildRequests', function() { +describe('BLIINK Adapter buildRequests', function () { for (const test of testsBuildRequests) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + test.args.after; + }); } -}) +}); const getSyncOptions = (pixelEnabled = true, iframeEnabled = false) => { return { pixelEnabled, - iframeEnabled - } -} + iframeEnabled, + }; +}; const getServerResponses = () => { return [ { - body: {bids: [], - userSyncs: [ { - type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' - }, - { - type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - }]}, - } - ] -} + body: { + bids: [], + userSyncs: [ + { + type: 'script', + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', + }, + { + type: 'image', + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], + }, + }, + ]; +}; const getGdprConsent = () => { return { gdprApplies: 1, consentString: 'XXX', - apiVersion: 2 - } -} + apiVersion: 2, + }; +}; const testsGetUserSyncs = [ { title: 'Should not have gdprConsent exist', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', }, { type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - } - ] + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], }, { title: 'Should return iframe cookie sync if iframeEnabled', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${ + getGdprConsent().apiVersion + }`, }, - ] + ], }, { title: 'ccpa', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent(), 'ccpa-consent') + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent(), + 'ccpa-consent' + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&uspConsent=ccpa-consent&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&uspConsent=ccpa-consent&gdprConsent=${ + getGdprConsent().consentString + }&apiVersion=${getGdprConsent().apiVersion}`, }, - ] + ], }, { title: 'Should output sync if no gdprConsent', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()) + fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()), }, - want: getServerResponses()[0].body.userSyncs - } -] + want: getServerResponses()[0].body.userSyncs, + }, + { + title: 'Should output empty array if no pixelEnabled', + args: { + fn: spec.getUserSyncs({}, getServerResponses()), + }, + want: [], + }, +]; -describe('BLIINK Adapter getUserSyncs', function() { +describe('BLIINK Adapter getUserSyncs', function () { for (const test of testsGetUserSyncs) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); + +describe('BLIINK Adapter keywords & coppa true', function () { + it('Should build request with keyword and coppa true if exist', () => { + const metaElement = document.createElement('meta'); + metaElement.name = 'keywords'; + metaElement.content = 'Bliink, Saber, Prebid'; + sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const querySelectorStub = sinon + .stub(document, 'querySelector') + .returns(metaElement); + expect( + spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ) + ).to.eql({ + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + coppa: 1, + gdprConsent: 'XXXX', + pageDescription: 'Bliink, Saber, Prebid', + pageTitle: '', + keywords: 'Bliink,Saber,Prebid', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }); + querySelectorStub.restore(); + config.getConfig.restore(); + }); +}); + +describe('getEffectiveConnectionType', () => { + let navigatorStub; + + beforeEach(() => { + if ('connection' in navigator) { + navigatorStub = sinon.stub(navigator, 'connection').value({ + effectiveType: undefined, + }); + } + }); + + afterEach(() => { + if (navigatorStub) { + navigatorStub.restore(); + } + }); + if (navigatorStub) { + it('should return "unsupported" when effective connection type is undefined', () => { + const result = getEffectiveConnectionType(); + expect(result).to.equal('unsupported'); + }); + } +}); + +it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(GVL_ID); +}); diff --git a/test/spec/modules/boldwinBidAdapter_spec.js b/test/spec/modules/boldwinBidAdapter_spec.js index 5b51183ea6d..9a7b16c0914 100644 --- a/test/spec/modules/boldwinBidAdapter_spec.js +++ b/test/spec/modules/boldwinBidAdapter_spec.js @@ -19,7 +19,8 @@ describe('BoldwinBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -110,6 +111,36 @@ describe('BoldwinBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -283,7 +314,7 @@ describe('BoldwinBidAdapter', function () { expect(userSync[0].type).to.exist; expect(userSync[0].url).to.exist; expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://cs.videowalldirect.com'); + expect(userSync[0].url).to.be.equal('https://sync.videowalldirect.com'); }); }); }); diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index 907c672208f..72a2e4b029c 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -67,6 +67,8 @@ const NO_USP_CONSENT = { usp: '1NYY' }; +const UNDEFINED_USER_CONSENT = {}; + function mockSurveyLoaded(surveyConf) { const commands = window._brandmetrics || []; commands.forEach(command => { @@ -120,6 +122,10 @@ describe('BrandmetricsRTD module', () => { it('should not init when there is no usp- consent', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); }); + + it('should init if there are no consent- objects defined', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, UNDEFINED_USER_CONSENT)).to.equal(true); + }); }); describe('getBidRequestData', () => { diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 75120aa7505..5fcc78f4322 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -89,12 +89,6 @@ describe('browsi Real time data sub module', function () { expect(browsiRTD.browsiSubmodule.getTargetingData([], null, null, auction)).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'], null, null, auction)).to.eql({adMock: {bv: 'NA'}}); - }); - it('should return prediction from server', function () { makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); const data = { diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index eb127cfd9f3..3ccb5405552 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -1,7 +1,8 @@ -import { expect } from 'chai'; -import { spec } from 'modules/cadentApertureMXBidAdapter.js'; import * as utils from 'src/utils.js'; + +import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/cadentApertureMXBidAdapter.js'; describe('cadent_aperture_mx Adapter', function () { describe('callBids', function () { @@ -236,228 +237,312 @@ describe('cadent_aperture_mx Adapter', function () { 'bidId': '30b31c2501de1e', 'auctionId': 'e19f1eff-8b27-42a6-888d-9674e5a6130c', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'ortb2Imp': { + 'ext': { + 'tid': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ed', + }, + }, }] }; let request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('sends bid request to ENDPOINT via POST', function () { - expect(request.method).to.equal('POST'); - }); + describe('non-gpp tests', function() { + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST'); + }); - it('contains the correct options', function () { - expect(request.options.withCredentials).to.equal(true); - }); + it('contains the correct options', function () { + expect(request.options.withCredentials).to.equal(true); + }); - it('contains a properly formatted endpoint url', function () { - const url = request.url.split('?'); - const queryParams = url[1].split('&'); - expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); - expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); - }); + it('contains a properly formatted endpoint url', function () { + const url = request.url.split('?'); + const queryParams = url[1].split('&'); + expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); + expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); + }); - it('builds bidfloor value from bid param when getFloor function does not exist', function () { - const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithFloor[0].params.bidfloor = 1; - const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); - const data = JSON.parse(requestWithFloor.data); - expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); - }); + it('builds bidfloor value from bid param when getFloor function does not exist', function () { + const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithFloor[0].params.bidfloor = 1; + const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); + const data = JSON.parse(requestWithFloor.data); + expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); + }); - it('builds bidfloor value from getFloor function when it exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithGetFloor[0].getFloor = () => floorResponse; - const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); - const data = JSON.parse(requestWithGetFloor.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor function when it exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithGetFloor[0].getFloor = () => floorResponse; + const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); + const data = JSON.parse(requestWithGetFloor.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); - bidRequestWithBothFloors[0].params.bidfloor = 1; - bidRequestWithBothFloors[0].getFloor = () => floorResponse; - const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); - const data = JSON.parse(requestWithBothFloors.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); + bidRequestWithBothFloors[0].params.bidfloor = 1; + bidRequestWithBothFloors[0].getFloor = () => floorResponse; + const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); + const data = JSON.parse(requestWithBothFloors.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('empty bidfloor value when floor and getFloor is not defined', function () { - const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); - const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); - const data = JSON.parse(requestWithoutFloor.data); - expect(data.imp[0].bidfloor).to.not.exist; - }); + it('empty bidfloor value when floor and getFloor is not defined', function () { + const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); + const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); + const data = JSON.parse(requestWithoutFloor.data); + expect(data.imp[0].bidfloor).to.not.exist; + }); - it('builds request properly', function () { - const data = JSON.parse(request.data); - expect(Array.isArray(data.imp)).to.equal(true); - expect(data.id).to.equal(bidderRequest.auctionId); - expect(data.imp.length).to.equal(1); - expect(data.imp[0].id).to.equal('30b31c2501de1e'); - expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ec'); - expect(data.imp[0].tagid).to.equal('25251'); - expect(data.imp[0].secure).to.equal(0); - expect(data.imp[0].vastXml).to.equal(undefined); - }); + it('builds request properly', function () { + const data = JSON.parse(request.data); + expect(Array.isArray(data.imp)).to.equal(true); + expect(data.id).to.equal(bidderRequest.auctionId); + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal('30b31c2501de1e'); + expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ed'); + expect(data.imp[0].tagid).to.equal('25251'); + expect(data.imp[0].secure).to.equal(0); + expect(data.imp[0].vastXml).to.equal(undefined); + }); - it('properly sends site information and protocol', function () { - request = spec.buildRequests(bidderRequest.bids, bidderRequest); - request = JSON.parse(request.data); - expect(request.site).to.have.property('domain', 'example.com'); - expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); - expect(request.site).to.have.property('ref', 'https://referrer.com'); - }); + it('populates id even when auctionId is not available', function () { + // addressing https://github.com/prebid/Prebid.js/issues/9781 + bidderRequest.auctionId = null; + request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('builds correctly formatted request banner object', function () { - let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); - let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.equal(undefined); - expect(data.imp[0].banner).to.exist.and.to.be.a('object'); - expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); - expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); - }); + const data = JSON.parse(request.data); + expect(data.id).not.to.be.null; + expect(data.id).not.to.equal(bidderRequest.auctionId); + }); - it('builds correctly formatted request video object for instream', function () { - let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithVideo[0].mediaTypes = { - video: { - context: 'instream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('properly sends site information and protocol', function () { + request = spec.buildRequests(bidderRequest.bids, bidderRequest); + request = JSON.parse(request.data); + expect(request.site).to.have.property('domain', 'example.com'); + expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); + expect(request.site).to.have.property('ref', 'https://referrer.com'); + }); - it('builds correctly formatted request video object for outstream', function () { - let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithOutstreamVideo[0].mediaTypes = { - video: { - context: 'outstream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithOutstreamVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('builds correctly formatted request banner object', function () { + let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); + let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.equal(undefined); + expect(data.imp[0].banner).to.exist.and.to.be.a('object'); + expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); + expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); + }); - it('shouldn\'t contain a user obj without GDPR information', function () { - let request = spec.buildRequests(bidderRequest.bids, bidderRequest) - request = JSON.parse(request.data) - expect(request).to.not.have.property('user'); - }); + it('builds correctly formatted request video object for instream', function () { + let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithVideo[0].mediaTypes = { + video: { + context: 'instream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); + }); - it('should have the right gdpr info when enabled', function () { - let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; - const gdprBidderRequest = utils.deepClone(bidderRequest); - gdprBidderRequest.gdprConsent = { - 'consentString': consentString, - 'gdprApplies': true - }; - let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + it('builds correctly formatted request video object for outstream', function () { + let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithOutstreamVideo[0].mediaTypes = { + video: { + context: 'outstream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithOutstreamVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); + }); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 1); - expect(request.user.ext).to.have.property('consent', consentString); - }); + it('shouldn\'t contain a user obj without GDPR information', function () { + let request = spec.buildRequests(bidderRequest.bids, bidderRequest) + request = JSON.parse(request.data) + expect(request).to.not.have.property('user'); + }); - it('should\'t contain consent string if gdpr isn\'t applied', function () { - const nonGdprBidderRequest = utils.deepClone(bidderRequest); - nonGdprBidderRequest.gdprConsent = { - 'gdprApplies': false - }; - let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 0); - expect(request).to.not.have.property('user'); - }); + it('should have the right gdpr info when enabled', function () { + let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; + const gdprBidderRequest = utils.deepClone(bidderRequest); + gdprBidderRequest.gdprConsent = { + 'consentString': consentString, + 'gdprApplies': true + }; + let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 1); + expect(request.user.ext).to.have.property('consent', consentString); + }); - it('should add us privacy info to request', function() { - const uspBidderRequest = utils.deepClone(bidderRequest); - let consentString = '1YNN'; - uspBidderRequest.uspConsent = consentString; - let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); - request = JSON.parse(request.data); - expect(request.us_privacy).to.exist; - expect(request.us_privacy).to.exist.and.to.equal(consentString); - }); + it('should\'t contain consent string if gdpr isn\'t applied', function () { + const nonGdprBidderRequest = utils.deepClone(bidderRequest); + nonGdprBidderRequest.gdprConsent = { + 'gdprApplies': false + }; + let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 0); + expect(request).to.not.have.property('user'); + }); - it('should add schain object to request', function() { - const schainBidderRequest = utils.deepClone(bidderRequest); - schainBidderRequest.bids[0].schain = { - 'complete': 1, - 'ver': '1.0', - 'nodes': [ - { - 'asi': 'testing.com', - 'sid': 'abc', - 'hp': 1 - } - ] - }; - let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); - request = JSON.parse(request.data); - expect(request.source.ext.schain).to.exist; - expect(request.source.ext.schain).to.have.property('complete', 1); - expect(request.source.ext.schain).to.have.property('ver', '1.0'); - expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); - }); + it('should add us privacy info to request', function() { + const uspBidderRequest = utils.deepClone(bidderRequest); + let consentString = '1YNN'; + uspBidderRequest.uspConsent = consentString; + let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); + request = JSON.parse(request.data); + expect(request.us_privacy).to.exist; + expect(request.us_privacy).to.exist.and.to.equal(consentString); + }); - it('should add liveramp identitylink id to request', () => { - const idl_env = '123'; - const bidRequestWithID = utils.deepClone(bidderRequest); - bidRequestWithID.userId = { idl_env }; - let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); - requestWithID = JSON.parse(requestWithID.data); - expect(requestWithID.user.ext.eids[0]).to.deep.equal({ - source: 'liveramp.com', - uids: [{ - id: idl_env, - ext: { - rtiPartner: 'idl' - } - }] + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.bids[0].schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); + }); + + it('should add liveramp identitylink id to request', () => { + const idl_env = '123'; + const bidRequestWithID = utils.deepClone(bidderRequest); + bidRequestWithID.userId = { idl_env }; + let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); + requestWithID = JSON.parse(requestWithID.data); + expect(requestWithID.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); + + it('should add gpid to request if present', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; + bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); }); - }); - it('should add gpid to request if present', () => { - const gpid = '/12345/my-gpt-tag-0'; - let bid = utils.deepClone(bidderRequest.bids[0]); - bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; - bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; - let requestWithGPID = spec.buildRequests([bid], bidderRequest); - requestWithGPID = JSON.parse(requestWithGPID.data); - expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + it('should add UID 2.0 to request', () => { + const uid2 = { id: '456' }; + const bidRequestWithUID = utils.deepClone(bidderRequest); + bidRequestWithUID.userId = { uid2 }; + let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); + requestWithUID = JSON.parse(requestWithUID.data); + expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: uid2.id, + ext: { + rtiPartner: 'UID2' + } + }] + }); + }); }); - it('should add UID 2.0 to request', () => { - const uid2 = { id: '456' }; - const bidRequestWithUID = utils.deepClone(bidderRequest); - bidRequestWithUID.userId = { uid2 }; - let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); - requestWithUID = JSON.parse(requestWithUID.data); - expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ - source: 'uidapi.com', - uids: [{ - id: uid2.id, - ext: { - rtiPartner: 'UID2' - } - }] + describe('gpp tests', function() { + describe('when gppConsent is not present on bid request', () => { + it('should return request with no gpp or gpp_sid properties', function() { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('when gppConsent is present on bid request', () => { + describe('gppString', () => { + describe('is not defined on request', () => { + it('should return request with gpp undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const gppString = 'abcdefgh'; + gppCompliantBidderRequest.gppConsent = { + gppString + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp).to.exist.and.to.equal(gppString); + }); + }); + }); + + describe('applicableSections', () => { + describe('is not defined on request', () => { + it('should return request with gpp_sid undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp_sid set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const applicableSections = [8]; + gppCompliantBidderRequest.gppConsent = { + applicableSections + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp_sid).to.deep.equal(applicableSections); + }); + }); + }); }); }); }); @@ -764,5 +849,38 @@ describe('cadent_aperture_mx Adapter', function () { expect(syncs[0].url).to.contains('usp=test'); expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test') }); + + it('should pass gpp string and section id', function() { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, {}, {}, { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs[0].url).to.contains('gpp=abcdefgs') + expect(syncs[0].url).to.contains('gpp_sid=1,2,4') + }); + + it('should pass us_privacy and gdpr string and gpp string', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, + { + gdprApplies: true, + consentString: 'test' + }, + { + consentString: 'test' + }, + { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + } + ); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('gdpr=1'); + expect(syncs[0].url).to.contains('usp=test'); + expect(syncs[0].url).to.contains('gpp=abcdefgs'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test&gpp=abcdefgs&gpp_sid=1,2,4'); + }); }); }); diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index 2301d6aab1b..d4f6aa66c7d 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -2,18 +2,16 @@ import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categ import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; +import {server} from '../../mocks/xhr.js'; describe('category translation', function () { - let fakeTranslationServer; let getLocalStorageStub; beforeEach(function () { - fakeTranslationServer = sinon.fakeServer.create(); getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { - fakeTranslationServer.reset(); getLocalStorageStub.restore(); config.resetConfig(); }); @@ -73,7 +71,7 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(0); + expect(server.requests.length).to.equal(0); clock.restore(); }); @@ -86,15 +84,15 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(1); + expect(server.requests.length).to.equal(1); clock.restore(); }); it('should use default mapping file if publisher has not defined in config', function () { getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(1); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); it('should use publisher defined mapping file', function () { @@ -105,7 +103,7 @@ describe('category translation', function () { }); getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(2); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(2); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); }); diff --git a/test/spec/modules/cointrafficBidAdapter_spec.js b/test/spec/modules/cointrafficBidAdapter_spec.js index 79775f7b135..21f02b4f8ef 100644 --- a/test/spec/modules/cointrafficBidAdapter_spec.js +++ b/test/spec/modules/cointrafficBidAdapter_spec.js @@ -4,6 +4,11 @@ import { spec } from 'modules/cointrafficBidAdapter.js'; import { config } from 'src/config.js' import * as utils from 'src/utils.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ + const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; describe('cointrafficBidAdapter', function () { diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index b8c872d879d..ebe1e9be4d4 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -255,13 +255,46 @@ describe('ColossussspAdapter', function () { }); describe('buildRequests with user ids', function () { - bid.userId = {} - bid.userId.britepoolid = 'britepoolid123'; - bid.userId.idl_env = 'idl_env123'; - bid.userId.tdid = 'tdid123'; - bid.userId.id5id = { uid: 'id5id123' }; - bid.userId.uid2 = { id: 'uid2id123' }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + var clonedBid = JSON.parse(JSON.stringify(bid)); + clonedBid.userId = {} + clonedBid.userId.britepoolid = 'britepoolid123'; + clonedBid.userId.idl_env = 'idl_env123'; + clonedBid.userId.tdid = 'tdid123'; + clonedBid.userId.id5id = { uid: 'id5id123' }; + clonedBid.userId.uid2 = { id: 'uid2id123' }; + clonedBid.userIdAsEids = [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '4679e98e-1d83-4718-8aba-aa88hhhaaa', + 'atype': 1 + } + ] + }, + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'e804908e-57b4-4f46-a097-08be44321e79', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + }, + { + 'source': 'neustar.biz', + 'uids': [ + { + 'id': 'E1:Bvss1x8hXM2zHeqiqj2umJUziavSvLT6E_ORri5fDCsZb-5sfD18oNWycTmdx6QBNdbURBVv466hLJiKSwHCaTxvROo8smjqj6GfvlKfzQI', + 'atype': 1 + } + ] + } + ]; + let serverRequest = spec.buildRequests([clonedBid], bidderRequest); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; let placements = data['placements']; @@ -270,11 +303,11 @@ describe('ColossussspAdapter', function () { let placement = placements[i]; expect(placement).to.have.property('eids') expect(placement.eids).to.be.an('array') - expect(placement.eids.length).to.be.equal(5) + expect(placement.eids.length).to.be.equal(8) for (let index in placement.eids) { let v = placement.eids[index]; expect(v).to.have.all.keys('source', 'uids') - expect(v.source).to.be.oneOf(['britepool.com', 'identityLink', 'adserver.org', 'id5-sync.com', 'uidapi.com']) + expect(v.source).to.be.oneOf(['pubcid.org', 'adserver.org', 'neustar.biz', 'britepool.com', 'identityLink', 'id5-sync.com', 'adserver.org', 'uidapi.com']) expect(v.uids).to.be.an('array'); expect(v.uids.length).to.be.equal(1) expect(v.uids[0]).to.have.property('id') diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 96c98e5e5a2..0a76ed3e62d 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -94,7 +94,7 @@ describe('ConcertAdapter', function () { }); describe('spec.isBidRequestValid', function() { - it('should return when it recieved all the required params', function() { + it('should return when it received all the required params', function() { const bid = bidRequests[0]; expect(spec.isBidRequestValid(bid)).to.equal(true); }); @@ -116,7 +116,20 @@ describe('ConcertAdapter', function () { expect(payload).to.have.property('meta'); expect(payload).to.have.property('slots'); - const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent']; + const metaRequiredFields = [ + 'prebidVersion', + 'pageUrl', + 'screen', + 'debug', + 'uid', + 'optedOut', + 'adapterVersion', + 'uspConsent', + 'gdprConsent', + 'gppConsent', + 'browserLanguage', + 'tdid' + ]; const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; metaRequiredFields.forEach(function(field) { @@ -199,6 +212,31 @@ describe('ConcertAdapter', function () { expect(slot.offsetCoordinates.x).to.equal(100) expect(slot.offsetCoordinates.y).to.equal(0) }) + + it('should not pass along tdid if the user has opted out', function() { + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should not pass along tdid if USP consent disallows', function() { + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, { ...bidRequest, uspConsent: '1YY' }); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should pass along tdid if the user has not opted out', function() { + storage.removeDataFromLocalStorage('c_nap', 'true'); + const tdid = '123abc'; + const bidRequestsWithTdid = [{ ...bidRequests[0], userId: { tdid } }] + const request = spec.buildRequests(bidRequestsWithTdid, bidRequest); + const payload = JSON.parse(request.data); + expect(payload.meta.tdid).to.equal(tdid); + }); }); describe('spec.interpretResponse', function() { diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js new file mode 100644 index 00000000000..4d816c4e816 --- /dev/null +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -0,0 +1,355 @@ +import { expect } from 'chai'; +import { + spec, + getBidFloor as connatixGetBidFloor +} from '../../../modules/connatixBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +describe('connatixBidAdapter', function () { + let bid; + + function mockBidRequest() { + const mediaTypes = { + banner: { + sizes: [16, 9], + } + }; + return { + bidId: 'testing', + bidder: 'connatix', + params: { + placementId: '30e91414-545c-4f45-a950-0bec9308ff22' + }, + mediaTypes + }; + }; + + describe('isBidRequestValid', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return true if all required fileds are present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if bidder does not correspond', function () { + bid.bidder = 'abc'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if bidId is missing', function () { + delete bid.bidId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if params object is missing', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if placementId is missing from params', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if mediaTypes is missing', function () { + delete bid.mediaTypes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if banner is missing from mediaTypes ', function () { + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is missing from banner object', function () { + delete bid.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is not an array', function () { + bid.mediaTypes.banner.sizes = 'test'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is an empty array', function () { + bid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return true if add an extra field was added to the bidRequest', function () { + bid.params.test = 1; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest; + let bidderRequest = { + refererInfo: { + canonicalUrl: '', + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {}, + gdprApplies: true + }, + uspConsent: '1YYY', + gppConsent: { + gppString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + applicableSections: [7] + }, + ortb2: { + site: { + data: { + pageType: 'article' + } + } + } + }; + + this.beforeEach(function () { + bid = mockBidRequest(); + 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://capi.connatix.com/rtb/hba'); + }); + it('Returns request payload', function () { + expect(serverRequest.data).to.not.empty; + }); + it('Validate request payload', function () { + expect(serverRequest.data.bidRequests[0].bidId).to.equal(bid.bidId); + expect(serverRequest.data.bidRequests[0].placementId).to.equal(bid.params.placementId); + expect(serverRequest.data.bidRequests[0].floor).to.equal(0); + expect(serverRequest.data.bidRequests[0].mediaTypes).to.equal(bid.mediaTypes); + expect(serverRequest.data.bidRequests[0].sizes).to.equal(bid.mediaTypes.sizes); + expect(serverRequest.data.refererInfo).to.equal(bidderRequest.refererInfo); + expect(serverRequest.data.gdprConsent).to.equal(bidderRequest.gdprConsent); + expect(serverRequest.data.uspConsent).to.equal(bidderRequest.uspConsent); + expect(serverRequest.data.gppConsent).to.equal(bidderRequest.gppConsent); + expect(serverRequest.data.ortb2).to.equal(bidderRequest.ortb2); + }); + }); + + describe('interpretResponse', function () { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; + + let serverResponse; + this.beforeEach(function () { + serverResponse = { + body: { + Bids: [ Bid ] + }, + headers: function() { } + }; + }); + + it('Should return an empty array if Bids is null', function () { + serverResponse.body.Bids = null; + + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return an empty array if Bids is empty array', function () { + serverResponse.body.Bids = []; + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return one bid response for one bid', function() { + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(1); + }); + + it('Should contains the same values as in the serverResponse', function() { + const bidResponses = spec.interpretResponse(serverResponse); + + const [ bidResponse ] = bidResponses; + expect(bidResponse.requestId).to.equal(serverResponse.body.Bids[0].RequestId); + expect(bidResponse.cpm).to.equal(serverResponse.body.Bids[0].Cpm); + expect(bidResponse.ttl).to.equal(serverResponse.body.Bids[0].Ttl); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.netRevenue).to.be.true; + }); + + it('Should return n bid responses for n bids', function() { + serverResponse.body.Bids = [ { ...Bid }, { ...Bid } ]; + + const firstBidCpm = 4; + serverResponse.body.Bids[0].Cpm = firstBidCpm; + + const secondBidCpm = 13; + serverResponse.body.Bids[1].Cpm = secondBidCpm; + + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(2); + + expect(bidResponses[0].cpm).to.equal(firstBidCpm); + expect(bidResponses[1].cpm).to.equal(secondBidCpm); + }); + }); + + describe('getUserSyncs', function() { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const UserSyncEndpoint = 'https://connatix.com/sync' + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; + + const serverResponse = { + body: { + UserSyncEndpoint, + Bids: [ Bid ] + }, + headers: function() { } + }; + + it('Should return an empty array when iframeEnabled: false', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when serverResponses is emprt array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an empty array', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an not defined or null', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, undefined, {}, {}, {})).to.be.an('array').that.is.empty; + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, null, {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return one user sync object when iframeEnabled is true and serverResponses is not an empry array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.not.empty; + }); + it('Should return a list containing a single object having type: iframe and url: syncUrl', function () { + const userSyncList = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], undefined, undefined, undefined); + const { type, url } = userSyncList[0]; + expect(type).to.equal('iframe'); + expect(url).to.equal(UserSyncEndpoint); + }); + it('Should append gdpr: 0 if gdprConsent object is provided but gdprApplies field is not provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=0`); + }); + it('Should append gdpr having the value of gdprApplied if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1`); + }); + it('Should append gdpr_consent if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'alabala'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=alabala`); + }); + it('Should encodeURI gdpr_consent corectly', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262`); + }); + it('Should append usp_consent to the url if uspConsent is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + it('Should not modify the sync url if gppConsent param is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + {consent: '1'} + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + }); + + describe('getBidFloor', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return 0 if both getFloor method and bidfloor param from bid are absent.', function () { + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + + it('Should return the value of the bidfloor parameter if the getFloor method is not defined but the bidfloor parameter is defined', function () { + const floorValue = 3; + bid.params.bidfloor = floorValue; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if the getFloor method is defined but the bidfloor parameter is not defined', function () { + const floorValue = 7; + bid.getFloor = function() { + return { floor: floorValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if both getFloor method and bidfloor parameter are defined', function () { + const floorParamValue = 3; + bid.params.bidfloor = floorParamValue; + + const floorMethodValue = 7; + bid.getFloor = function() { + return { floor: floorMethodValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorMethodValue); + }); + + it('Should return 0 if the getFloor method is defined and it crash when call it', function () { + bid.getFloor = function() { + throw new Error('error'); + }; + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index 5376ba60886..686c3d63a63 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -3,6 +3,7 @@ import {connectIdSubmodule, storage} from 'modules/connectIdSystem.js'; import {server} from '../../mocks/xhr'; import {parseQS, parseUrl} from 'src/utils.js'; import {uspDataHandler, gppDataHandler} from 'src/adapterManager.js'; +import * as refererDetection from '../../../src/refererDetection'; const TEST_SERVER_URL = 'http://localhost:9876/'; @@ -288,6 +289,79 @@ describe('Yahoo ConnectID Submodule', () => { expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); }); + it('returns an object with the stored ID from cookies and syncs because of expired TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 10000; + const cookieData = {connectId: 'foo', he: 'email', lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL with provided puid', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + puid: '9' + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and syncs because is O&O traffic', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + const getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + getRefererInfoStub.returns({ + ref: 'https://dev.fc.yahoo.com?test' + }); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + getRefererInfoStub.restore(); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + it('Makes an ajax GET request to the production API endpoint with stored puid when id is stale', () => { const last15Days = Date.now() - (60 * 60 * 24 * 1000 * 15); const last29Days = Date.now() - (60 * 60 * 24 * 1000 * 29); diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 17f8f6f6eac..93a876d0233 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -1,19 +1,23 @@ import { - setConsentConfig, + consentTimeout, + GPPClient, requestBidsHook, resetConsentData, - userCMP, - consentTimeout, - storeConsentData, lookupIabConsent + setConsentConfig, + userCMP } from 'modules/consentManagementGpp.js'; -import { gppDataHandler } from 'src/adapterManager.js'; +import {gppDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import 'src/prebid.js'; +import {MODE_CALLBACK, MODE_MIXED} from '../../../libraries/cmp/cmpClient.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; let expect = require('chai').expect; describe('consentManagementGpp', function () { + beforeEach(resetConsentData); + describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -101,64 +105,6 @@ describe('consentManagementGpp', function () { }); }); - describe('lookupIABConsent', () => { - let mockCmp, mockCmpEvent, gppData, sectionData - beforeEach(() => { - gppData = { - gppString: 'mockString', - applicableSections: [], - pingData: {} - }; - sectionData = {}; - mockCmp = sinon.stub().callsFake(({command, callback, parameter}) => { - let res; - switch (command) { - case 'addEventListener': - mockCmpEvent = callback; - break; - case 'getGPPData': - res = gppData; - break; - case 'getSection': - res = sectionData[parameter]; - break; - } - return Promise.resolve(res); - }); - }) - - function runLookup() { - return new Promise((resolve, reject) => lookupIabConsent({onSuccess: resolve, onError: reject}, () => mockCmp)); - } - - function oneShotLookup() { - const pm = runLookup(); - mockCmpEvent({eventName: 'sectionChange'}); - return pm; - } - - it('fetches all sections', () => { - gppData.pingData.supportedAPIs = ['usnat', 'usca'] - sectionData = { - usnat: {mock: 'usnat'}, - usca: {mock: 'usca'} - }; - return oneShotLookup().then((res) => { - expect(res.sectionData).to.eql(sectionData); - }); - }); - - it('does not choke if some section data is not available', () => { - gppData.pingData.supportedAPIs = ['usnat', 'usca'] - sectionData = { - usca: {mock: 'data'} - }; - return oneShotLookup().then((res) => { - expect(res.sectionData).to.eql(sectionData); - }) - }); - }) - describe('static consent string setConsentConfig value', () => { afterEach(() => { config.resetConfig(); @@ -169,17 +115,19 @@ describe('consentManagementGpp', function () { gpp: { cmpApi: 'static', timeout: 7500, - sectionData: { - usnat: { - MockUsnatParsedFlag: true - } - }, consentData: { applicableSections: [7], gppString: 'ABCDEFG1234', gppVersion: 1, sectionId: 3, - sectionList: [] + sectionList: [], + parsedSections: { + usnat: [ + { + MockUsnatParsedFlag: true + }, + ] + }, } } }; @@ -193,6 +141,599 @@ describe('consentManagementGpp', function () { }); }); }); + describe('GPPClient.ping', () => { + function mkPingData(gppVersion) { + return { + gppVersion + } + } + Object.entries({ + 'unknown': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData(), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.0': { + expectedMode: MODE_MIXED, + pingData: mkPingData('1.0'), + apiVersion: '1.0', + client() { + return this.pingData; + } + }, + '1.1 that runs callback immediately': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.1 that defers callback': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + }, + '> 1.1': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.2'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + } + }).forEach(([t, scenario]) => { + describe(`using CMP version ${t}`, () => { + let clients, mkClient; + beforeEach(() => { + clients = []; + mkClient = ({mode}) => { + const mockClient = function (args) { + if (args.command === 'ping') { + return Promise.resolve(scenario.client(args)); + } + } + mockClient.mode = mode; + mockClient.close = sinon.stub(); + clients.push(mockClient); + return mockClient; + } + }); + + it('should resolve to client with the correct mode', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.cmp.mode).to.eql(scenario.expectedMode); + }); + }); + + it('should resolve to pingData', () => { + return GPPClient.ping(mkClient).then(([_, pingData]) => { + expect(pingData).to.eql(scenario.pingData); + }); + }); + + it('should .close the probing client', () => { + return GPPClient.ping(mkClient).then(([client]) => { + sinon.assert.called(clients[0].close); + sinon.assert.notCalled(client.cmp.close); + }) + }); + + it('should .tag the client with version', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.apiVersion).to.eql(scenario.apiVersion); + }) + }) + }) + }); + + it('should reject when mkClient returns null (CMP not found)', () => { + return GPPClient.ping(() => null).catch((err) => { + expect(err.message).to.match(/not found/); + }); + }); + + it('should reject when client rejects', () => { + const err = {some: 'prop'}; + const mockClient = () => Promise.reject(err); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }); + }); + + it('should reject when callback is invoked with success = false', () => { + const err = 'error'; + const mockClient = ({callback}) => callback(err, false); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }) + }) + }); + + describe('GPPClient.init', () => { + let makeCmp, cmpCalls, cmpResult; + + beforeEach(() => { + cmpResult = {signalStatus: 'ready', gppString: 'mock-str'}; + cmpCalls = []; + makeCmp = sinon.stub().callsFake(() => { + function mockCmp(args) { + cmpCalls.push(args); + return GreedyPromise.resolve(cmpResult); + } + mockCmp.close = sinon.stub(); + return mockCmp; + }); + }); + + it('should re-use same client', (done) => { + GPPClient.init(makeCmp).then(([client]) => { + GPPClient.init(makeCmp).then(([client2, consentPm]) => { + expect(client2).to.equal(client); + expect(cmpCalls.filter((el) => el.command === 'ping').length).to.equal(2) // recycled client should be refreshed + consentPm.then((consent) => { + expect(consent.gppString).to.eql('mock-str'); + done() + }) + }); + }); + }); + + it('should not re-use errors', (done) => { + cmpResult = GreedyPromise.reject(new Error()); + GPPClient.init(makeCmp).catch(() => { + cmpResult = {signalStatus: 'ready'}; + return GPPClient.init(makeCmp).then(([client]) => { + expect(client).to.exist; + done() + }) + }) + }) + }) + + describe('GPP client', () => { + const CHANGE_EVENTS = ['sectionChange', 'signalStatus']; + + let gppClient, gppData, cmpReady, eventListener; + + function mockClient(apiVersion = '1.1', cmpVersion = '1.1') { + const mockCmp = sinon.stub().callsFake(function ({command, callback}) { + if (command === 'addEventListener') { + eventListener = callback; + } else { + throw new Error('unexpected command: ' + command); + } + }) + const client = new GPPClient(cmpVersion, mockCmp); + client.apiVersion = apiVersion; + client.getGPPData = sinon.stub().callsFake(() => Promise.resolve(gppData)); + client.isCMPReady = sinon.stub().callsFake(() => cmpReady); + client.events = CHANGE_EVENTS; + return client; + } + + beforeEach(() => { + gppDataHandler.reset(); + eventListener = null; + cmpReady = true; + gppData = { + applicableSections: [7], + gppString: 'mock-string', + parsedSections: { + usnat: [ + { + Field: 'val' + }, + { + SubsectionType: 1, + Gpc: false + } + ] + } + }; + gppClient = mockClient(); + }); + + describe('updateConsent', () => { + it('should update data handler with consent data', () => { + return gppClient.updateConsent().then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + expect(gppDataHandler.ready).to.be.true; + }); + }); + + Object.entries({ + 'emtpy': {}, + 'missing': null + }).forEach(([t, data]) => { + it(`should not update, and reject promise, when gpp data is ${t}`, (done) => { + gppData = data; + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/empty/); + expect(err.args).to.eql(data == null ? [] : [data]); + expect(gppDataHandler.ready).to.be.false; + done() + }) + }); + }) + + it('should not update when gpp data rejects', (done) => { + gppData = Promise.reject(new Error('err')); + gppClient.updateConsent().catch(err => { + expect(gppDataHandler.ready).to.be.false; + expect(err.message).to.eql('err'); + done(); + }) + }); + + describe('consent data validation', () => { + Object.entries({ + applicableSections: { + 'not an array': 'not-an-array', + }, + gppString: { + 'not a string': 234 + }, + parsedSections: { + 'not an object': 'not-an-object' + } + }).forEach(([prop, tests]) => { + describe(`validation: when ${prop} is`, () => { + Object.entries(tests).forEach(([t, value]) => { + describe(t, () => { + it('should not update', (done) => { + Object.assign(gppData, {[prop]: value}); + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/unexpected/); + expect(err.args).to.eql([gppData]); + expect(gppDataHandler.ready).to.be.false; + done(); + }); + }); + }) + }); + }); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + gppClient.isCMPReady = function (pingData) { + return pingData.ready; + } + gppClient.getGPPData = function (pingData) { + return Promise.resolve(pingData); + } + }) + + it('does not use initial pingData if CMP is not ready', () => { + gppClient.init({...gppData, ready: false}); + expect(eventListener).to.exist; + expect(gppDataHandler.ready).to.be.false; + }); + + it('uses initial pingData (and resolves promise) if CMP is ready', () => { + return gppClient.init({...gppData, ready: true}).then(data => { + expect(eventListener).to.exist; + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }) + }); + + it('rejects promise when CMP errors out', (done) => { + gppClient.init({ready: false}).catch((err) => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql(['error']) + done(); + }); + eventListener('error', false); + }); + + Object.entries({ + 'empty': {}, + 'null': null, + 'irrelevant': {eventName: 'irrelevant'} + }).forEach(([t, evt]) => { + it(`ignores ${t} events`, () => { + let pm = gppClient.init({ready: false}).catch((err) => err.args[0] !== 'done' && Promise.reject(err)); + eventListener(evt); + eventListener('done', false); + return pm; + }) + }); + + it('rejects the promise when cmpStatus is "error"', (done) => { + const evt = {eventName: 'other', pingData: {cmpStatus: 'error'}}; + gppClient.init({ready: false}).catch(err => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql([evt]); + done(); + }); + eventListener(evt); + }) + + CHANGE_EVENTS.forEach(evt => { + describe(`event: ${evt}`, () => { + function makeEvent(pingData) { + return { + eventName: evt, + pingData + } + } + + let gppData2 + beforeEach(() => { + gppData2 = Object.assign(gppData, {gppString: '2nd'}); + }); + + it('does not fire consent data updates if the CMP is not ready', (done) => { + gppClient.init({ready: false}).catch(() => { + expect(gppDataHandler.ready).to.be.false; + done(); + }); + eventListener({...gppData2, ready: false}); + eventListener('done', false); + }) + + it('fires consent data updates (and resolves promise) if CMP is ready', (done) => { + gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData2); + done() + }); + cmpReady = true; + eventListener(makeEvent({...gppData2, ready: true})); + }); + + it('keeps updating consent data on new events', () => { + let pm = gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }); + eventListener(makeEvent({...gppData, ready: true})); + return pm.then(() => { + eventListener(makeEvent({...gppData2, ready: true})) + }).then(() => { + sinon.assert.match(gppDataHandler.getConsentData(), gppData2); + }); + }); + }) + }) + }); + }); + + describe('GPP 1.0 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.0'))('1.0', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'loaded': [true, 'loaded'], + 'other': [false, 'other'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, cmpStatus]]) => { + it(`should be ${expected} when cmpStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {cmpStatus}))).to.equal(expected); + }); + }); + }); + + describe('getGPPData', () => { + let gppData, pingData; + beforeEach(() => { + gppData = { + gppString: 'mock-string', + supportedAPIs: ['usnat'], + applicableSections: [7, 8] + } + pingData = { + supportedAPIs: gppData.supportedAPIs + }; + }); + + function mockCmpCommands(commands) { + mockCmp.callsFake(({command, parameter}) => { + if (commands.hasOwnProperty((command))) { + return Promise.resolve(commands[command](parameter)); + } else { + return Promise.reject(new Error(`unrecognized command ${command}`)) + } + }) + } + + it('should retrieve consent string and applicableSections', () => { + mockCmpCommands({ + getGPPData: () => gppData + }) + return gppClient.getGPPData(pingData).then(data => { + sinon.assert.match(data, gppData); + }) + }); + + it('should reject when getGPPData rejects', (done) => { + mockCmpCommands({ + getGPPData: () => Promise.reject(new Error('err')) + }); + gppClient.getGPPData(pingData).catch(err => { + expect(err.message).to.eql('err'); + done(); + }); + }); + + it('should not choke if supportedAPIs is missing', () => { + [gppData, pingData].forEach(ob => { delete ob.supportedAPIs; }) + mockCmpCommands({ + getGPPData: () => gppData + }); + return gppClient.getGPPData(pingData).then(res => { + expect(res.gppString).to.eql(gppData.gppString); + expect(res.parsedSections).to.eql({}); + }) + }) + + describe('section data', () => { + let usnat, parsedUsnat; + + function mockSections(sections) { + mockCmpCommands({ + getGPPData: () => gppData, + getSection: (api) => (sections[api]) + }); + }; + + beforeEach(() => { + usnat = { + MockField: 'val', + OtherField: 'o', + Gpc: true + }; + parsedUsnat = [ + { + MockField: 'val', + OtherField: 'o' + }, + { + SubsectionType: 1, + Gpc: true + } + ] + }); + + it('retrieves section data', () => { + mockSections({usnat}); + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}) + }); + }); + + it('does not choke if a section is missing', () => { + mockSections({usnat}); + gppData.supportedAPIs = ['usnat', 'missing']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + + it('does not choke if a section fails', () => { + mockSections({usnat, err: Promise.reject(new Error('err'))}); + gppData.supportedAPIs = ['usnat', 'err']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + }) + }); + }); + + describe('GPP 1.1 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.1'))('1.1', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'ready': [true, 'ready'], + 'not ready': [false, 'not ready'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, signalStatus]]) => { + it(`should be ${expected} when signalStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {signalStatus}))).to.equal(expected); + }); + }); + }); + + it('gets GPPData from pingData', () => { + mockCmp.throws(new Error()); + const pingData = { + 'gppVersion': '1.1', + 'cmpStatus': 'loaded', + 'cmpDisplayStatus': 'disabled', + 'supportedAPIs': [ + '5:tcfcav1', + '7:usnat', + '8:usca', + '9:usva', + '10:usco', + '11:usut', + '12:usct' + ], + 'signalStatus': 'ready', + 'cmpId': 31, + 'sectionList': [ + 7 + ], + 'applicableSections': [ + 7 + ], + 'gppString': 'DBABL~BAAAAAAAAgA.QA', + 'parsedSections': { + 'usnat': [ + { + 'Version': 1, + 'SharingNotice': 0, + 'SaleOptOutNotice': 0, + 'SharingOptOutNotice': 0, + 'TargetedAdvertisingOptOutNotice': 0, + 'SensitiveDataProcessingOptOutNotice': 0, + 'SensitiveDataLimitUseNotice': 0, + 'SaleOptOut': 0, + 'SharingOptOut': 0, + 'TargetedAdvertisingOptOut': 0, + 'SensitiveDataProcessing': [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + 'KnownChildSensitiveDataConsents': [ + 0, + 0 + ], + 'PersonalDataConsents': 0, + 'MspaCoveredTransaction': 2, + 'MspaOptOutOptionMode': 0, + 'MspaServiceProviderMode': 0 + }, + { + 'SubsectionType': 1, + 'Gpc': false + } + ] + } + }; + return gppClient.getGPPData(pingData).then((gppData) => { + sinon.assert.match(gppData, { + gppString: pingData.gppString, + applicableSections: pingData.applicableSections, + parsedSections: pingData.parsedSections + }) + }) + }) + }) describe('requestBidsHook tests:', function () { let goodConfig = { @@ -297,13 +838,13 @@ describe('consentManagementGpp', function () { }); it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + window.__gpp = function () {}; setConsentConfig({ gpp: { cmpApi: 'iab', timeout: 0 } }); - window.__gpp = function () {}; try { requestBidsHook(() => { const consent = gppDataHandler.getConsentData(); @@ -320,14 +861,16 @@ describe('consentManagementGpp', function () { describe('already known consentData:', function () { let cmpStub = sinon.stub(); - function mockCMP(cmpResponse) { - return function (...args) { - if (args[0] === 'addEventListener') { - args[1](({ - eventName: 'sectionChange' - })); - } else if (args[0] === 'getGPPData') { - return cmpResponse; + function mockCMP(pingData) { + return function (command, callback) { + switch (command) { + case 'addEventListener': + // eslint-disable-next-line standard/no-callback-literal + callback({eventName: 'sectionChange', pingData}) + break; + case 'ping': + callback(pingData) + break; } } } @@ -350,7 +893,7 @@ describe('consentManagementGpp', function () { gppString: 'xyz', }; - cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP(testConsentData)); + cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP({...testConsentData, signalStatus: 'ready'})); setConsentConfig(goodConfig); requestBidsHook(() => {}, {}); cmpStub.reset(); @@ -366,289 +909,5 @@ describe('consentManagementGpp', function () { sinon.assert.notCalled(cmpStub); }); }); - - describe('iframe tests', function () { - let cmpPostMessageCb = () => {}; - let stringifyResponse; - - function createIFrameMarker(frameName) { - let ifr = document.createElement('iframe'); - ifr.width = 0; - ifr.height = 0; - ifr.name = frameName; - document.body.appendChild(ifr); - return ifr; - } - - function creatCmpMessageHandler(prefix, returnEvtValue, returnGPPValue) { - return function (event) { - if (event && event.data) { - let data = event.data; - if (data[`${prefix}Call`]) { - let callId = data[`${prefix}Call`].callId; - let response; - if (data[`${prefix}Call`].command === 'addEventListener') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnEvtValue, - success: true - } - } - } else if (data[`${prefix}Call`].command === 'getGPPData') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnGPPValue, - success: true - } - } - } else if (data[`${prefix}Call`].command === 'getSection') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: {}, - success: true - } - } - } - event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); - } - } - } - } - - function testIFramedPage(testName, messageFormatString, tarConsentString, tarSections) { - it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { - stringifyResponse = messageFormatString; - setConsentConfig(goodConfig); - requestBidsHook(() => { - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(consent.gppString).to.equal(tarConsentString); - expect(consent.applicableSections).to.deep.equal(tarSections); - done(); - }, {}); - }); - } - - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); - config.resetConfig(); - resetConsentData(); - }); - - describe('workflow for iframe pages:', function () { - stringifyResponse = false; - let ifr2 = null; - - beforeEach(function () { - ifr2 = createIFrameMarker('__gppLocator'); - cmpPostMessageCb = creatCmpMessageHandler('__gpp', { - eventName: 'sectionChange' - }, { - gppString: 'abc12345234', - applicableSections: [7] - }); - window.addEventListener('message', cmpPostMessageCb, false); - }); - - afterEach(function () { - delete window.__gpp; // deletes the local copy made by the postMessage CMP call function - document.body.removeChild(ifr2); - window.removeEventListener('message', cmpPostMessageCb); - }); - - testIFramedPage('with/JSON response', false, 'abc12345234', [7]); - testIFramedPage('with/String response', true, 'abc12345234', [7]); - }); - }); - - describe('direct calls to CMP API tests', function () { - let cmpStub = sinon.stub(); - - beforeEach(function () { - didHookReturn = false; - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - config.resetConfig(); - cmpStub.restore(); - utils.logError.restore(); - utils.logWarn.restore(); - resetConsentData(); - }); - - describe('CMP workflow for normal pages:', function () { - beforeEach(function () { - window.__gpp = function () {}; - }); - - afterEach(function () { - delete window.__gpp; - }); - - it('performs lookup check and stores consentData for a valid existing user', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; - expect(consent.gppString).to.equal(testConsentData.gppString); - expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); - }); - - it('produces gdpr metadata', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consentMeta = gppDataHandler.getConsentMeta(); - sinon.assert.notCalled(utils.logError); - expect(consentMeta.generatedAt).to.be.above(1644367751709); - }); - - it('throws an error when processCmpData check fails + does not call requestBids callback', function () { - let testConsentData = {}; - let bidsBackHandlerReturn = false; - - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - - [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); - - requestBidsHook(() => { - didHookReturn = true; - }, { - bidsBackHandler: () => bidsBackHandlerReturn = true - }); - let consent = gppDataHandler.getConsentData(); - - sinon.assert.calledOnce(utils.logError); - sinon.assert.notCalled(utils.logWarn); - expect(didHookReturn).to.be.false; - expect(bidsBackHandlerReturn).to.be.true; - expect(consent).to.be.null; - expect(gppDataHandler.ready).to.be.true; - }); - - describe('when proper consent is not available', () => { - let gppStub; - - function runAuction() { - setConsentConfig({ - gpp: { - cmpApi: 'iab', - timeout: 10, - } - }); - return new Promise((resolve, reject) => { - requestBidsHook(() => { - didHookReturn = true; - }, {}); - setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); - }) - } - - function mockGppCmp(gppdata) { - gppStub.callsFake((api, cb) => { - if (api === 'addEventListener') { - // eslint-disable-next-line standard/no-callback-literal - cb({ - pingData: { - cmpStatus: 'loaded' - } - }, true); - } - if (api === 'getGPPData') { - return gppdata; - } - }); - } - - beforeEach(() => { - gppStub = sinon.stub(window, '__gpp'); - }); - - afterEach(() => { - gppStub.restore(); - }) - - it('should continue auction with null consent when CMP is unresponsive', () => { - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([]); - expect(consent.gppString).to.be.undefined; - expect(gppDataHandler.ready).to.be.true; - }); - }); - - it('should use consent provided by events other than sectionChange', () => { - mockGppCmp({ - gppString: 'mock-consent-string', - applicableSections: [7] - }); - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([7]); - expect(consent.gppString).to.equal('mock-consent-string'); - expect(gppDataHandler.ready).to.be.true; - }); - }); - }); - }); - }); }); }); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index e98486754ab..c372c66f7f0 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -522,6 +522,19 @@ describe('consentManagement', function () { setConsentConfig(goodConfig); expect(uspDataHandler.getConsentData()).to.eql('string'); }); + + it('does not invoke registerDeletion if the CMP calls back with an error', () => { + sandbox.stub(window, '__uspapi').callsFake((cmd, _, cb) => { + if (cmd === 'registerDeletion') { + cb(null, false); + } else { + // eslint-disable-next-line standard/no-callback-literal + cb({uspString: 'string'}, true); + } + }); + setConsentConfig(goodConfig); + sinon.assert.notCalled(adapterManager.callDataDeletionRequest); + }) }); }); }); diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index 556dce447b9..d8e75454245 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -53,6 +53,10 @@ const BIDDER_REQUEST_1 = { consentString: 'consent-test', gdprApplies: false }, + gppConsent: { + applicableSections: [1, 2], + gppString: 'consent-string' + }, refererInfo: { referer: 'http://example.com/page.html', reachedTop: true, @@ -647,10 +651,30 @@ describe('Consumable BidAdapter', function () { expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=0&gdpr_consent=GDPR_CONSENT_STRING'); }) - it('should return a sync url if iframe syncs are enabled and USP applies', function () { - let uspConsent = { - consentString: 'USP_CONSENT_STRING', + it('should return a sync url if iframe syncs are enabled and has GPP consent with applicable sections', function () { + let gppConsent = { + applicableSections: [1, 2], + gppString: 'GPP_CONSENT_STRING' + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING&gpp_sid=1%2C2'); + }) + + it('should return a sync url if iframe syncs are enabled and has GPP consent without applicable sections', function () { + let gppConsent = { + applicableSections: [], + gppString: 'GPP_CONSENT_STRING' } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled and USP applies', function () { + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, uspConsent); expect(opts.length).to.equal(1); @@ -662,9 +686,7 @@ describe('Consumable BidAdapter', function () { consentString: 'GDPR_CONSENT_STRING', gdprApplies: true, } - let uspConsent = { - consentString: 'USP_CONSENT_STRING', - } + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent, uspConsent); expect(opts.length).to.equal(1); @@ -689,50 +711,22 @@ describe('Consumable BidAdapter', function () { sandbox.restore(); }); - it('Request should have unifiedId config params', function() { + it('Request should have EIDs', function() { bidderRequest.bidRequest[0].userId = {}; bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); - let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ + bidderRequest.bidRequest[0].userIdAsEids = [{ 'source': 'adserver.org', 'uids': [{ - 'id': 'TTD_ID', + 'id': 'TTD_ID_FROM_USER_ID_MODULE', 'atype': 1, 'ext': { 'rtiPartner': 'TDID' } }] - }]); - }); - - it('Request should have adsrvrOrgId from UserId Module if config and userId module both have TTD ID', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - adsrvrOrgId: { - 'TDID': 'TTD_ID_FROM_CONFIG', - 'TDID_LOOKUP': 'TRUE', - 'TDID_CREATED_AT': '2022-06-21T09:47:00' - } - }; - return config[key]; - }); - bidderRequest.bidRequest[0].userId = {}; - bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); + }]; let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]); + expect(data.user.eids).to.deep.equal(bidderRequest.bidRequest[0].userIdAsEids); }); it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { diff --git a/test/spec/modules/contxtfulRtdProvider_spec.js b/test/spec/modules/contxtfulRtdProvider_spec.js new file mode 100644 index 00000000000..541c0e6e6dd --- /dev/null +++ b/test/spec/modules/contxtfulRtdProvider_spec.js @@ -0,0 +1,200 @@ +import { contxtfulSubmodule } from '../../../modules/contxtfulRtdProvider.js'; +import { expect } from 'chai'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +import * as events from '../../../src/events'; + +const _ = null; +const VERSION = 'v1'; +const CUSTOMER = 'CUSTOMER'; +const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/p.js`; +const INITIAL_RECEPTIVITY = { ReceptivityState: 'INITIAL_RECEPTIVITY' }; +const INITIAL_RECEPTIVITY_EVENT = new CustomEvent('initialReceptivity', { detail: INITIAL_RECEPTIVITY }); + +const CONTXTFUL_API = { GetReceptivity: sinon.stub() } +const RX_ENGINE_IS_READY_EVENT = new CustomEvent('rxEngineIsReady', {detail: CONTXTFUL_API}); + +function buildInitConfig(version, customer) { + return { + name: 'contxtful', + params: { + version, + customer, + }, + }; +} + +describe('contxtfulRtdProvider', function () { + let sandbox = sinon.sandbox.create(); + let loadExternalScriptTag; + let eventsEmitSpy; + + beforeEach(() => { + loadExternalScriptTag = document.createElement('script'); + loadExternalScriptStub.callsFake((_url, _moduleName) => loadExternalScriptTag); + + CONTXTFUL_API.GetReceptivity.reset(); + + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + afterEach(function () { + delete window.Contxtful; + sandbox.restore(); + }); + + describe('extractParameters with invalid configuration', () => { + const { + params: { customer, version }, + } = buildInitConfig(VERSION, CUSTOMER); + const theories = [ + [ + null, + 'params.version should be a non-empty string', + 'null object for config', + ], + [ + {}, + 'params.version should be a non-empty string', + 'empty object for config', + ], + [ + { customer }, + 'params.version should be a non-empty string', + 'customer only in config', + ], + [ + { version }, + 'params.customer should be a non-empty string', + 'version only in config', + ], + [ + { customer, version: '' }, + 'params.version should be a non-empty string', + 'empty string for version', + ], + [ + { customer: '', version }, + 'params.customer should be a non-empty string', + 'empty string for customer', + ], + [ + { customer: '', version: '' }, + 'params.version should be a non-empty string', + 'empty string for version & customer', + ], + ]; + + theories.forEach(([params, expectedErrorMessage, _description]) => { + const config = { name: 'contxtful', params }; + it('throws the expected error', () => { + expect(() => contxtfulSubmodule.extractParameters(config)).to.throw( + expectedErrorMessage + ); + }); + }); + }); + + describe('initialization with invalid config', function () { + it('returns false', () => { + expect(contxtfulSubmodule.init({})).to.be.false; + }); + }); + + describe('initialization with valid config', function () { + it('returns true when initializing', () => { + const config = buildInitConfig(VERSION, CUSTOMER); + expect(contxtfulSubmodule.init(config)).to.be.true; + }); + + it('loads contxtful module script asynchronously', (done) => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + + setTimeout(() => { + expect(loadExternalScriptStub.calledOnce).to.be.true; + expect(loadExternalScriptStub.args[0][0]).to.equal( + CONTXTFUL_CONNECTOR_ENDPOINT + ); + done(); + }, 10); + }); + }); + + describe('load external script return falsy', function () { + it('returns true when initializing', () => { + loadExternalScriptStub.callsFake(() => {}); + const config = buildInitConfig(VERSION, CUSTOMER); + expect(contxtfulSubmodule.init(config)).to.be.true; + }); + }); + + describe('rxEngine from external script', function () { + it('use rxEngine api to get receptivity', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(RX_ENGINE_IS_READY_EVENT); + + contxtfulSubmodule.getTargetingData(['ad-slot']); + + expect(CONTXTFUL_API.GetReceptivity.calledOnce).to.be.true; + }); + }); + + describe('initial receptivity is not dispatched', function () { + it('does not initialize receptivity value', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); + expect(targetingData).to.deep.equal({}); + }); + }); + + describe('initial receptivity is invalid', function () { + const theories = [ + [new Event('initialReceptivity'), 'event without details'], + [new CustomEvent('initialReceptivity', { }), 'custom event without details'], + [new CustomEvent('initialReceptivity', { detail: {} }), 'custom event with invalid details'], + [new CustomEvent('initialReceptivity', { detail: { ReceptivityState: '' } }), 'custom event with details without ReceptivityState'], + ]; + + theories.forEach(([initialReceptivityEvent, _description]) => { + it('does not initialize receptivity value', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(initialReceptivityEvent); + + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); + expect(targetingData).to.deep.equal({}); + }); + }) + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + { 'ad-slot': { ReceptivityState: 'INITIAL_RECEPTIVITY' } }, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + 'ad-slot-1': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + 'ad-slot-2': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, _description]) => { + it('adds "ReceptivityState" to the adUnits', function () { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(INITIAL_RECEPTIVITY_EVENT); + + expect(contxtfulSubmodule.getTargetingData(adUnits)).to.deep.equal( + expected + ); + }); + }); + }); +}); diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index ce134f7f6af..f425535ce73 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import {expect} from 'chai'; import {default as conversantAnalytics, CNVR_CONSTANTS, cnvrHelper} from 'modules/conversantAnalyticsAdapter'; import * as utils from 'src/utils.js'; import * as prebidGlobal from 'src/prebidGlobal'; +import {server} from '../../mocks/xhr.js'; import constants from 'src/constants.json' @@ -10,14 +11,13 @@ let events = require('src/events'); describe('Conversant analytics adapter tests', function() { let sandbox; // sinon sandbox to make restoring all stubbed objects easier - let xhr; // xhr stub from sinon for capturing data sent via ajax let clock; // clock stub from sinon to mock our cache cleanup interval let logInfoStub; const PREBID_VERSION = '1.2'; const SITE_ID = 108060; - let requests = []; + let requests; const DATESTAMP = Date.now(); const VALID_CONFIGURATION = { @@ -36,10 +36,9 @@ describe('Conversant analytics adapter tests', function() { }; beforeEach(function () { + requests = server.requests; sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); // need to stub this otherwise unwanted events seem to get fired during testing - xhr = sandbox.useFakeXMLHttpRequest(); // allows us to capture ajax requests - xhr.onCreate = function (req) { requests.push(req); }; // save ajax requests in a private array for testing purposes let getGlobalStub = { version: PREBID_VERSION, getUserIds: function() { // userIdTargeting.js init() gets called on AUCTION_END so we need to mock this function. @@ -60,7 +59,6 @@ describe('Conversant analytics adapter tests', function() { afterEach(function () { sandbox.restore(); - requests = []; // clean up any requests in our ajax request capture array. conversantAnalytics.disableAnalytics(); }); diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 59ebefa2d60..9503a050092 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -2,13 +2,20 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/conversantBidAdapter.js'; import * as utils from 'src/utils.js'; import {createEidsArray} from 'modules/userId/eids.js'; -import { config } from '../../../src/config.js'; import {deepAccess} from 'src/utils'; +// load modules that register ORTB processors +import 'src/prebid.js' +import 'modules/currency.js'; +import 'modules/userId/index.js'; // handles eids +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; // handles schain +import {hook} from '../../../src/hook.js' describe('Conversant adapter tests', function() { const siteId = '108060'; const versionPattern = /^\d+\.\d+\.\d+(.)*$/; - const bidRequests = [ // banner with single size { @@ -19,13 +26,18 @@ describe('Conversant adapter tests', function() { tag_id: 'tagid-1', bidfloor: 0.5 }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, placementCode: 'pcode000', transactionId: 'tx000', - sizes: [[300, 250]], bidId: 'bid000', bidderRequestId: '117d765b87bed38', auctionId: 'req000' }, + // banner with sizes in mediaTypes.banner.sizes { bidder: 'conversant', @@ -51,9 +63,13 @@ describe('Conversant adapter tests', function() { position: 2, tag_id: '' }, + mediaTypes: { + banner: { + sizes: [[300, 600], [160, 600]], + } + }, placementCode: 'pcode002', transactionId: 'tx002', - sizes: [[300, 600], [160, 600]], bidId: 'bid002', bidderRequestId: '117d765b87bed38', auctionId: 'req000' @@ -77,7 +93,6 @@ describe('Conversant adapter tests', function() { }, placementCode: 'pcode003', transactionId: 'tx003', - sizes: [640, 480], bidId: 'bid003', bidderRequestId: '117d765b87bed38', auctionId: 'req000' @@ -125,16 +140,15 @@ describe('Conversant adapter tests', function() { bidderRequestId: '117d765b87bed38', auctionId: 'req000' }, - // video with first party data + // banner with first party data { bidder: 'conversant', params: { site_id: siteId }, mediaTypes: { - video: { - context: 'instream', - mimes: ['video/mp4', 'video/x-flv'] + banner: { + sizes: [[300, 600], [160, 600]], } }, ortb2Imp: { @@ -150,23 +164,6 @@ describe('Conversant adapter tests', function() { bidId: 'bid006', bidderRequestId: '117d765b87bed38', auctionId: 'req000' - }, - { - bidder: 'conversant', - params: { - site_id: siteId - }, - mediaTypes: { - banner: { - sizes: [[728, 90], [468, 60]], - pos: 5 - } - }, - placementCode: 'pcode001', - transactionId: 'tx001', - bidId: 'bid007', - bidderRequestId: '117d765b87bed38', - auctionId: 'req000' } ]; @@ -217,7 +214,14 @@ describe('Conversant adapter tests', function() { }] }] }, - headers: {}}; + headers: {} + }; + + before(() => { + // ortbConverter depends on other modules to be setup to work as expected so run hook.ready to register some + // submodules so functions like setOrtbSourceExtSchain and setOrtbUserExtEids are available + hook.ready(); + }); it('Verify basic properties', function() { expect(spec.code).to.equal('conversant'); @@ -232,12 +236,9 @@ describe('Conversant adapter tests', function() { expect(spec.isBidRequestValid({})).to.be.false; expect(spec.isBidRequestValid({params: {}})).to.be.false; expect(spec.isBidRequestValid({params: {site_id: '123'}})).to.be.true; - expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[1])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[2])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[3])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[4])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[5])).to.be.true; + bidRequests.forEach((bid) => { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); const simpleVideo = JSON.parse(JSON.stringify(bidRequests[3])); simpleVideo.params.site_id = 123; @@ -251,152 +252,171 @@ describe('Conversant adapter tests', function() { expect(spec.isBidRequestValid(simpleVideo)).to.be.true; }); - it('Verify buildRequest', function() { - const page = 'http://test.com?a=b&c=123'; - const bidderRequest = { - refererInfo: { - page: page - }, - ortb2: { - source: { - tid: 'tid000' + describe('Verify buildRequest', function() { + let page, bidderRequest, request, payload; + before(() => { + page = 'http://test.com?a=b&c=123'; + // ortbConverter uses the site/device information from the ortb2 object passed in the bidderRequest object + bidderRequest = { + refererInfo: { + page: page + }, + ortb2: { + source: { + tid: 'tid000' + }, + site: { + mobile: 0, + page: page, + }, + device: { + w: screen.width, + h: screen.height, + dnt: 0, + ua: navigator.userAgent + } } - } - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'); - const payload = request.data; + }; + request = spec.buildRequests(bidRequests, bidderRequest); + payload = request.data; + }); + + it('Verify common elements', function() { + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'); + + expect(payload).to.have.property('id'); + expect(payload.source).to.have.property('tid', 'tid000'); + expect(payload).to.have.property('at', 1); + expect(payload).to.have.property('imp'); + expect(payload.imp).to.be.an('array').with.lengthOf(bidRequests.length); + + expect(payload).to.have.property('site'); + expect(payload.site).to.have.property('id', siteId); + expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); + + expect(payload.site).to.have.property('page', page); + + expect(payload).to.have.property('device'); + expect(payload.device).to.have.property('w', screen.width); + expect(payload.device).to.have.property('h', screen.height); + expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]); + expect(payload.device).to.have.property('ua', navigator.userAgent); + + expect(payload).to.not.have.property('user'); // there should be no user by default + expect(payload).to.not.have.property('tmax'); // there should be no user by default + }); - expect(payload).to.have.property('id'); - expect(payload.source).to.have.property('tid', 'tid000'); - expect(payload).to.have.property('at', 1); - expect(payload).to.have.property('imp'); - expect(payload.imp).to.be.an('array').with.lengthOf(8); - - expect(payload.imp[0]).to.have.property('id', 'bid000'); - expect(payload.imp[0]).to.have.property('secure', 1); - expect(payload.imp[0]).to.have.property('bidfloor', 0.5); - expect(payload.imp[0]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[0]).to.have.property('tagid', 'tagid-1'); - expect(payload.imp[0]).to.have.property('banner'); - expect(payload.imp[0].banner).to.have.property('pos', 1); - expect(payload.imp[0].banner).to.have.property('format'); - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); - expect(payload.imp[0]).to.not.have.property('video'); - - expect(payload.imp[1]).to.have.property('id', 'bid001'); - expect(payload.imp[1]).to.have.property('secure', 1); - expect(payload.imp[1]).to.have.property('bidfloor', 0); - expect(payload.imp[1]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[1]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[1]).to.not.have.property('tagid'); - expect(payload.imp[1]).to.have.property('banner'); - expect(payload.imp[1].banner).to.not.have.property('pos'); - expect(payload.imp[1].banner).to.have.property('format'); - expect(payload.imp[1].banner.format).to.deep.equal([{w: 728, h: 90}, {w: 468, h: 60}]); - - expect(payload.imp[2]).to.have.property('id', 'bid002'); - expect(payload.imp[2]).to.have.property('secure', 1); - expect(payload.imp[2]).to.have.property('bidfloor', 0); - expect(payload.imp[2]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[2]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[2]).to.have.property('banner'); - expect(payload.imp[2].banner).to.have.property('pos', 2); - expect(payload.imp[2].banner).to.have.property('format'); - expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 600}, {w: 160, h: 600}]); - - expect(payload.imp[3]).to.have.property('id', 'bid003'); - expect(payload.imp[3]).to.have.property('secure', 1); - expect(payload.imp[3]).to.have.property('bidfloor', 0); - expect(payload.imp[3]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[3]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[3]).to.not.have.property('tagid'); - expect(payload.imp[3]).to.have.property('video'); - expect(payload.imp[3].video).to.have.property('pos', 3); - expect(payload.imp[3].video).to.have.property('w', 632); - expect(payload.imp[3].video).to.have.property('h', 499); - expect(payload.imp[3].video).to.have.property('mimes'); - expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[3].video).to.have.property('protocols'); - expect(payload.imp[3].video.protocols).to.deep.equal([1, 2]); - expect(payload.imp[3].video).to.have.property('api'); - expect(payload.imp[3].video.api).to.deep.equal([2]); - expect(payload.imp[3].video).to.have.property('maxduration', 30); - expect(payload.imp[3]).to.not.have.property('banner'); - - expect(payload.imp[4]).to.have.property('id', 'bid004'); - expect(payload.imp[4]).to.have.property('secure', 1); - expect(payload.imp[4]).to.have.property('bidfloor', 0); - expect(payload.imp[4]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[4]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[4]).to.not.have.property('tagid'); - expect(payload.imp[4]).to.have.property('video'); - expect(payload.imp[4].video).to.not.have.property('pos'); - expect(payload.imp[4].video).to.have.property('w', 1024); - expect(payload.imp[4].video).to.have.property('h', 768); - expect(payload.imp[4].video).to.have.property('mimes'); - expect(payload.imp[4].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[4].video).to.have.property('protocols'); - expect(payload.imp[4].video.protocols).to.deep.equal([1, 2, 3]); - expect(payload.imp[4].video).to.have.property('api'); - expect(payload.imp[4].video.api).to.deep.equal([2, 3]); - expect(payload.imp[4].video).to.have.property('maxduration', 30); - expect(payload.imp[4]).to.not.have.property('banner'); - - expect(payload.imp[5]).to.have.property('id', 'bid005'); - expect(payload.imp[5]).to.have.property('secure', 1); - expect(payload.imp[5]).to.have.property('bidfloor', 0); - expect(payload.imp[5]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[5]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[5]).to.not.have.property('tagid'); - expect(payload.imp[5]).to.have.property('video'); - expect(payload.imp[5].video).to.have.property('pos', 2); - expect(payload.imp[5].video).to.not.have.property('w'); - expect(payload.imp[5].video).to.not.have.property('h'); - expect(payload.imp[5].video).to.have.property('mimes'); - expect(payload.imp[5].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[5].video).to.not.have.property('protocols'); - expect(payload.imp[5].video).to.not.have.property('api'); - expect(payload.imp[5].video).to.not.have.property('maxduration'); - expect(payload.imp[5]).to.not.have.property('banner'); - - expect(payload.imp[6]).to.have.property('id', 'bid006'); - expect(payload.imp[6]).to.have.property('video'); - expect(payload.imp[6].video).to.have.property('mimes'); - expect(payload.imp[6].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[6]).to.not.have.property('banner'); - expect(payload.imp[6]).to.have.property('instl'); - expect(payload.imp[6]).to.have.property('ext'); - expect(payload.imp[6].ext).to.have.property('data'); - expect(payload.imp[6].ext.data).to.have.property('pbadslot'); - - expect(payload.imp[7]).to.have.property('id', 'bid007'); - expect(payload.imp[7]).to.have.property('secure', 1); - expect(payload.imp[7]).to.have.property('bidfloor', 0); - expect(payload.imp[7]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[7]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[7]).to.not.have.property('tagid'); - expect(payload.imp[7]).to.have.property('banner'); - expect(payload.imp[7].banner).to.have.property('pos', 5); - expect(payload.imp[7].banner).to.have.property('format'); - expect(payload.imp[7].banner.format).to.deep.equal([{w: 728, h: 90}, {w: 468, h: 60}]); - - expect(payload).to.have.property('site'); - expect(payload.site).to.have.property('id', siteId); - expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); - - expect(payload.site).to.have.property('page', page); - - expect(payload).to.have.property('device'); - expect(payload.device).to.have.property('w', screen.width); - expect(payload.device).to.have.property('h', screen.height); - expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]); - expect(payload.device).to.have.property('ua', navigator.userAgent); - - expect(payload).to.not.have.property('user'); // there should be no user by default - expect(payload).to.not.have.property('tmax'); // there should be no user by default + it('Simple banner', () => { + expect(payload.imp[0]).to.have.property('id', 'bid000'); + expect(payload.imp[0]).to.have.property('secure', 1); + expect(payload.imp[0]).to.have.property('bidfloor', 0.5); + expect(payload.imp[0]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[0]).to.have.property('tagid', 'tagid-1'); + expect(payload.imp[0]).to.have.property('banner'); + expect(payload.imp[0].banner).to.have.property('pos', 1); + expect(payload.imp[0].banner).to.have.property('format'); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + expect(payload.imp[0]).to.not.have.property('video'); + }); + + it('Banner multiple sizes', () => { + expect(payload.imp[1]).to.have.property('id', 'bid001'); + expect(payload.imp[1]).to.have.property('secure', 1); + expect(payload.imp[1]).to.have.property('bidfloor', 0); + expect(payload.imp[1]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[1]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[1]).to.not.have.property('tagid'); + expect(payload.imp[1]).to.have.property('banner'); + expect(payload.imp[1].banner).to.not.have.property('pos'); + expect(payload.imp[1].banner).to.have.property('format'); + expect(payload.imp[1].banner.format).to.deep.equal([{w: 728, h: 90}, {w: 468, h: 60}]); + }); + + it('Banner with tagid and position', () => { + expect(payload.imp[2]).to.have.property('id', 'bid002'); + expect(payload.imp[2]).to.have.property('secure', 1); + expect(payload.imp[2]).to.have.property('bidfloor', 0); + expect(payload.imp[2]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[2]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[2]).to.have.property('banner'); + expect(payload.imp[2].banner).to.have.property('pos', 2); + expect(payload.imp[2].banner).to.have.property('format'); + expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 600}, {w: 160, h: 600}]); + }); + + if (FEATURES.VIDEO) { + it('Simple video', () => { + expect(payload.imp[3]).to.have.property('id', 'bid003'); + expect(payload.imp[3]).to.have.property('secure', 1); + expect(payload.imp[3]).to.have.property('bidfloor', 0); + expect(payload.imp[3]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[3]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[3]).to.not.have.property('tagid'); + expect(payload.imp[3]).to.have.property('video'); + expect(payload.imp[3].video).to.have.property('pos', 3); + expect(payload.imp[3].video).to.have.property('w', 632); + expect(payload.imp[3].video).to.have.property('h', 499); + expect(payload.imp[3].video).to.have.property('mimes'); + expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[3].video).to.have.property('protocols'); + expect(payload.imp[3].video.protocols).to.deep.equal([1, 2]); + expect(payload.imp[3].video).to.have.property('api'); + expect(payload.imp[3].video.api).to.deep.equal([2]); + expect(payload.imp[3].video).to.have.property('maxduration', 30); + expect(payload.imp[3]).to.not.have.property('banner'); + }); + + it('Video with playerSize', () => { + expect(payload.imp[4]).to.have.property('id', 'bid004'); + expect(payload.imp[4]).to.have.property('secure', 1); + expect(payload.imp[4]).to.have.property('bidfloor', 0); + expect(payload.imp[4]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[4]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[4]).to.not.have.property('tagid'); + expect(payload.imp[4]).to.have.property('video'); + expect(payload.imp[4].video).to.not.have.property('pos'); + expect(payload.imp[4].video).to.have.property('w', 1024); + expect(payload.imp[4].video).to.have.property('h', 768); + expect(payload.imp[4].video).to.have.property('mimes'); + expect(payload.imp[4].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[4].video).to.have.property('protocols'); + expect(payload.imp[4].video.protocols).to.deep.equal([1, 2, 3]); + expect(payload.imp[4].video).to.have.property('api'); + expect(payload.imp[4].video.api).to.deep.equal([1, 2, 3]); + expect(payload.imp[4].video).to.have.property('maxduration', 30); + }); + + it('Video without sizes', () => { + expect(payload.imp[5]).to.have.property('id', 'bid005'); + expect(payload.imp[5]).to.have.property('secure', 1); + expect(payload.imp[5]).to.have.property('bidfloor', 0); + expect(payload.imp[5]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[5]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[5]).to.not.have.property('tagid'); + expect(payload.imp[5]).to.have.property('video'); + expect(payload.imp[5].video).to.have.property('pos', 2); + expect(payload.imp[5].video).to.not.have.property('w'); + expect(payload.imp[5].video).to.not.have.property('h'); + expect(payload.imp[5].video).to.have.property('mimes'); + expect(payload.imp[5].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[5].video).to.not.have.property('protocols'); + expect(payload.imp[5].video).to.not.have.property('api'); + expect(payload.imp[5].video).to.not.have.property('maxduration'); + expect(payload.imp[5]).to.not.have.property('banner'); + }); + } + + it('With FPD', () => { + expect(payload.imp[6]).to.have.property('id', 'bid006'); + expect(payload.imp[6]).to.have.property('banner'); + expect(payload.imp[6]).to.not.have.property('video'); + expect(payload.imp[6]).to.have.property('instl'); + expect(payload.imp[6]).to.have.property('ext'); + expect(payload.imp[6].ext).to.have.property('data'); + expect(payload.imp[6].ext.data).to.have.property('pbadslot'); + }); }); it('Verify timeout', () => { @@ -440,59 +460,62 @@ describe('Conversant adapter tests', function() { expect(request.url).to.equal(testUrl); }); - it('Verify interpretResponse', function() { - const request = spec.buildRequests(bidRequests, {}); - const response = spec.interpretResponse(bidResponses, request); - expect(response).to.be.an('array').with.lengthOf(4); - - let bid = response[0]; - expect(bid).to.have.property('requestId', 'bid000'); - expect(bid).to.have.property('currency', 'USD'); - expect(bid).to.have.property('cpm', 0.99); - expect(bid).to.have.property('creativeId', '1000'); - expect(bid).to.have.property('width', 300); - expect(bid).to.have.property('height', 250); - expect(bid.meta.advertiserDomains).to.deep.equal(['https://example.com']); - expect(bid).to.have.property('ad', 'markup000'); - expect(bid).to.have.property('ttl', 300); - expect(bid).to.have.property('netRevenue', true); + describe('Verify interpretResponse', function() { + let bid, request, response; + + before(() => { + request = spec.buildRequests(bidRequests, {}); + response = spec.interpretResponse(bidResponses, request); + }); + + it('Banner', function() { + expect(response).to.be.an('array').with.lengthOf(4); + bid = response[0]; + expect(bid).to.have.property('requestId', 'bid000'); + expect(bid).to.have.property('cpm', 0.99); + expect(bid).to.have.property('creativeId', '1000'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 250); + expect(bid.meta.advertiserDomains).to.deep.equal(['https://example.com']); + expect(bid).to.have.property('ad', 'markup000
'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); + }); // There is no bid001 because cpm is $0 - bid = response[1]; - expect(bid).to.have.property('requestId', 'bid002'); - expect(bid).to.have.property('currency', 'USD'); - expect(bid).to.have.property('cpm', 2.99); - expect(bid).to.have.property('creativeId', '1002'); - expect(bid).to.have.property('width', 300); - expect(bid).to.have.property('height', 600); - expect(bid).to.have.property('ad', 'markup002'); - expect(bid).to.have.property('ttl', 300); - expect(bid).to.have.property('netRevenue', true); - - bid = response[2]; - expect(bid).to.have.property('requestId', 'bid003'); - expect(bid).to.have.property('currency', 'USD'); - expect(bid).to.have.property('cpm', 3.99); - expect(bid).to.have.property('creativeId', '1003'); - expect(bid).to.have.property('width', 632); - expect(bid).to.have.property('height', 499); - expect(bid).to.have.property('vastUrl', 'markup003'); - expect(bid).to.have.property('mediaType', 'video'); - expect(bid).to.have.property('ttl', 300); - expect(bid).to.have.property('netRevenue', true); - - bid = response[3]; - expect(bid).to.have.property('vastXml', ''); - }); + it('Banner multiple sizes', function() { + bid = response[1]; + expect(bid).to.have.property('requestId', 'bid002'); + expect(bid).to.have.property('cpm', 2.99); + expect(bid).to.have.property('creativeId', '1002'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 600); + expect(bid).to.have.property('ad', 'markup002
'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); + }); + + if (FEATURES.VIDEO) { + it('Video', function () { + bid = response[2]; + expect(bid).to.have.property('requestId', 'bid003'); + expect(bid).to.have.property('cpm', 3.99); + expect(bid).to.have.property('creativeId', '1003'); + expect(bid).to.have.property('playerWidth', 632); + expect(bid).to.have.property('playerHeight', 499); + expect(bid).to.have.property('vastUrl', 'notify003'); + expect(bid).to.have.property('vastXml', 'markup003'); + expect(bid).to.have.property('mediaType', 'video'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); + }); - it('Verify handling of bad responses', function() { - let response = spec.interpretResponse({}, {}); - expect(response).to.be.an('array').with.lengthOf(0); - response = spec.interpretResponse({id: '123'}, {}); - expect(response).to.be.an('array').with.lengthOf(0); - response = spec.interpretResponse({id: '123', seatbid: []}, {}); - expect(response).to.be.an('array').with.lengthOf(0); + it('Empty Video', function() { + bid = response[3]; + expect(bid).to.have.property('vastXml', ''); + }); + } }); it('Verify publisher commond id support', function() { @@ -524,79 +547,23 @@ describe('Conversant adapter tests', function() { expect(payload).to.not.have.nested.property('user.ext.eids'); }); - it('Verify GDPR bid request', function() { - // add gdpr info - const bidderRequest = { - gdprConsent: { - consentString: 'BOJObISOJObISAABAAENAA4AAAAAoAAA', - gdprApplies: true - } - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('user.ext.consent', 'BOJObISOJObISAABAAENAA4AAAAAoAAA'); - expect(payload).to.have.deep.nested.property('regs.ext.gdpr', 1); - }); - - it('Verify GDPR bid request without gdprApplies', function() { - // add gdpr info - const bidderRequest = { - gdprConsent: { - consentString: '' - } - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('user.ext.consent', ''); - expect(payload).to.not.have.deep.nested.property('regs.ext.gdpr'); - }); - - describe('CCPA', function() { - it('should have us_privacy', function() { - const bidderRequest = { - uspConsent: '1NYN' - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('regs.ext.us_privacy', '1NYN'); - expect(payload).to.not.have.deep.nested.property('regs.ext.gdpr'); - }); - - it('should have no us_privacy', function() { - const payload = spec.buildRequests(bidRequests, {}).data; - expect(payload).to.not.have.deep.nested.property('regs.ext.us_privacy'); - }); - - it('should have both gdpr and us_privacy', function() { - const bidderRequest = { - gdprConsent: { - consentString: 'BOJObISOJObISAABAAENAA4AAAAAoAAA', - gdprApplies: true - }, - uspConsent: '1NYN' - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('user.ext.consent', 'BOJObISOJObISAABAAENAA4AAAAAoAAA'); - expect(payload).to.have.deep.nested.property('regs.ext.gdpr', 1); - expect(payload).to.have.deep.nested.property('regs.ext.us_privacy', '1NYN'); - }); - }); - describe('Extended ID', function() { it('Verify unifiedid and liveramp', function() { // clone bidRequests let requests = utils.deepClone(bidRequests); + const uid = {pubcid: '112233', idl_env: '334455'}; + const eidArray = [{'source': 'pubcid.org', 'uids': [{'id': '112233', 'atype': 1}]}, {'source': 'liveramp.com', 'uids': [{'id': '334455', 'atype': 3}]}]; + // add pubcid to every entry requests.forEach((unit) => { - Object.assign(unit, {userId: {pubcid: '112233', tdid: '223344', idl_env: '334455'}}); - Object.assign(unit, {userIdAsEids: createEidsArray(unit.userId)}); + Object.assign(unit, {userId: uid}); + Object.assign(unit, {userIdAsEids: eidArray}); }); // construct http post payload const payload = spec.buildRequests(requests, {}).data; expect(payload).to.have.deep.nested.property('user.ext.eids', [ - {source: 'adserver.org', uids: [{id: '223344', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: '112233', atype: 1}]}, {source: 'liveramp.com', uids: [{id: '334455', atype: 3}]} ]); }); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index b2f3d64a156..2cdb09f2098 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -9,11 +9,12 @@ import { } from 'modules/criteoBidAdapter.js'; import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; +import * as ajax from 'src/ajax.js'; import { config } from '../../../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { - let utilsMock, sandbox; + let utilsMock, sandbox, ajaxStub; beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -26,6 +27,7 @@ describe('The Criteo bidding adapter', function () { utilsMock = sinon.mock(utils); sandbox = sinon.sandbox.create(); + ajaxStub = sandbox.stub(ajax, 'ajax'); }); afterEach(function () { @@ -33,6 +35,7 @@ describe('The Criteo bidding adapter', function () { global.Criteo = undefined; utilsMock.restore(); sandbox.restore(); + ajaxStub.restore(); }); describe('getUserSyncs', function () { @@ -56,7 +59,9 @@ describe('The Criteo bidding adapter', function () { cookiesAreEnabledStub, localStorageIsEnabledStub, getCookieStub, - getDataFromLocalStorageStub; + setCookieStub, + getDataFromLocalStorageStub, + removeDataFromLocalStorageStub; beforeEach(function () { getConfigStub = sinon.stub(config, 'getConfig'); @@ -75,8 +80,10 @@ describe('The Criteo bidding adapter', function () { localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); localStorageIsEnabledStub.returns(true); - getCookieStub = sinon.stub(storage, 'getCookie') + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + removeDataFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); }); afterEach(function () { @@ -86,7 +93,9 @@ describe('The Criteo bidding adapter', function () { cookiesAreEnabledStub.restore(); localStorageIsEnabledStub.restore(); getCookieStub.restore(); + setCookieStub.restore(); getDataFromLocalStorageStub.restore(); + removeDataFromLocalStorageStub.restore(); }); it('should not trigger sync if publisher is using fast bid', function () { @@ -198,6 +207,46 @@ describe('The Criteo bidding adapter', function () { url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com&us_privacy=ABC#${JSON.stringify(expectedHash).replace(/"/g, '%22')}` }]); }); + + it('should delete user data when calling onDataDeletionRequest', () => { + const cookieData = { + 'cto_bundle': 'a' + }; + const lsData = { + 'cto_bundle': 'a' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.equal(true); + expect(setCookieStub.calledOnce).to.equal(true); + expect(getDataFromLocalStorageStub.calledOnce).to.equal(true); + expect(removeDataFromLocalStorageStub.calledOnce).to.equal(true); + expect(cookieData.cto_bundle).to.equal(''); + expect(lsData.cto_bundle).to.equal(''); + expect(ajaxStub.calledOnce).to.equal(true); + }); + + it('should not call API when calling onDataDeletionRequest with no id', () => { + const cookieData = { + 'cto_bundle': '' + }; + const lsData = { + 'cto_bundle': '' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.be.true; + expect(setCookieStub.called).to.be.false; + expect(getDataFromLocalStorageStub.calledOnce).to.be.true + expect(removeDataFromLocalStorageStub.called).to.be.false; + expect(ajaxStub.called).to.be.false; + }); }); describe('isBidRequestValid', function () { @@ -315,93 +364,6 @@ describe('The Criteo bidding adapter', function () { }); it('should return false when given an invalid video bid request', function () { - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'instream', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 2, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'outstream', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'adpod', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - expect(spec.isBidRequestValid({ bidder: 'criteo', mediaTypes: { @@ -654,6 +616,33 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.source.tid).to.equal('abc'); }); + it('should properly transmit bidId if available', function () { + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.slots[0].slotid).to.equal('bidId'); + }); + it('should properly build a request if refererInfo is not provided', function () { const bidderRequest = {}; const bidRequests = [ @@ -1046,6 +1035,30 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user.uspIab).to.equal('1YNY'); }); + it('should properly build a request with site and app ortb fields', function () { + const bidRequests = []; + let app = { + publisher: { + id: 'appPublisherId' + } + }; + let site = { + publisher: { + id: 'sitePublisherId' + } + }; + const bidderRequest = { + ortb2: { + app: app, + site: site + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.app).to.equal(app); + expect(request.data.site).to.equal(site); + }); + it('should properly build a request with device sua field', function () { const sua = {} const bidRequests = [ @@ -1097,7 +1110,7 @@ describe('The Criteo bidding adapter', function () { const ortb2 = { regs: { gpp: 'gpp_consent_string', - gpp_sid: [0, 1, 2] + gpp_sid: [0, 1, 2], } }; @@ -1107,6 +1120,48 @@ describe('The Criteo bidding adapter', function () { expect(request.data.regs.gpp_sid).to.deep.equal([0, 1, 2]); }); + it('should properly build a request with dsa object', function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + }, + }, + ]; + let dsa = { + required: 3, + pubrender: 0, + datatopub: 2, + transparency: [{ + domain: 'platform1domain.com', + params: [1] + }, { + domain: 'SSP2domain.com', + params: [1, 2] + }] + }; + const ortb2 = { + regs: { + ext: { + dsa: dsa + } + } + }; + + const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2 }); + expect(request.data.regs).to.not.be.null; + expect(request.data.regs.ext).to.not.be.null; + expect(request.data.regs.ext.dsa).to.deep.equal(dsa); + }); + it('should properly build a request with schain object', function () { const expectedSchain = { someProperty: 'someValue' @@ -1250,12 +1305,25 @@ describe('The Criteo bidding adapter', function () { sizes: [[640, 480]], mediaTypes: { video: { + context: 'instream', playerSize: [640, 480], mimes: ['video/mp4', 'video/x-flv'], maxduration: 30, api: [1, 2], protocols: [2, 3], - plcmt: 3 + plcmt: 3, + w: 640, + h: 480, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], } }, params: { @@ -1274,6 +1342,7 @@ describe('The Criteo bidding adapter', function () { expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; + expect(ortbRequest.slots[0].video.context).to.equal('instream'); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480']); @@ -1286,6 +1355,18 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([1, 3]); expect(ortbRequest.slots[0].video.placement).to.equal(2); expect(ortbRequest.slots[0].video.plcmt).to.equal(3); + expect(ortbRequest.slots[0].video.w).to.equal(640); + expect(ortbRequest.slots[0].video.h).to.equal(480); + expect(ortbRequest.slots[0].video.linearity).to.equal(1); + expect(ortbRequest.slots[0].video.skipmin).to.equal(30); + expect(ortbRequest.slots[0].video.skipafter).to.equal(30); + expect(ortbRequest.slots[0].video.minbitrate).to.equal(10000); + expect(ortbRequest.slots[0].video.maxbitrate).to.equal(48000); + expect(ortbRequest.slots[0].video.delivery).to.deep.equal([1, 2, 3]); + expect(ortbRequest.slots[0].video.pos).to.equal(1); + expect(ortbRequest.slots[0].video.playbackend).to.equal(1); + expect(ortbRequest.slots[0].video.adPodDurationSec).to.equal(30); + expect(ortbRequest.slots[0].video.durationRangeSec).to.deep.equal([1, 30]); }); it('should properly build a video request with more than one player size', function () { @@ -1803,6 +1884,86 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].rwdd).to.be.undefined; }); + + it('should properly build a request when FLEDGE is enabled', function () { + const bidderRequest = { + fledgeEnabled: true, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext.ae).to.equal(1); + }); + + it('should properly build a request when FLEDGE is disabled', function () { + const bidderRequest = { + fledgeEnabled: false, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext).to.not.have.property('ae'); + }); + + it('should properly transmit device.ext.cdep if available', function () { + const bidderRequest = { + ortb2: { + device: { + ext: { + cdep: 'cookieDeprecationLabel' + } + } + } + }; + const bidRequests = []; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.device.ext.cdep).to.equal('cookieDeprecationLabel'); + }); }); describe('interpretResponse', function () { @@ -1837,6 +1998,11 @@ describe('The Criteo bidding adapter', function () { bidRequests: [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { networkId: 456, } @@ -1855,6 +2021,244 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].meta.networkName).to.equal('Criteo'); }); + it('should properly parse a bid response with dsa', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + ext: { + dsa: { + adrender: 1 + }, + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].meta.dsa.adrender).to.equal(1); + }); + + it('should properly parse a bid response with a networkId with twin ad unit banner win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 480], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId2'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].dealId).to.equal('myDealCode'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); + expect(bids[0].meta.networkName).to.equal('Criteo'); + }); + + it('should properly parse a bid response with a networkId with twin ad unit video win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + bidId: 'abc123', + cpm: 1.23, + displayurl: 'http://test-ad', + width: 728, + height: 90, + zoneid: 123, + video: true, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [728, 90], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].mediaType).to.equal(VIDEO); + }); + + it('should properly parse a bid response with a networkId with twin ad unit native win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + native: { + 'products': [{ + 'sendTargetingKeys': false, + 'title': 'Product title', + 'description': 'Product desc', + 'price': '100', + 'click_url': 'https://product.click', + 'image': { + 'url': 'https://publisherdirect.criteo.com/publishertag/preprodtest/creative.png', + 'height': 300, + 'width': 300 + }, + 'call_to_action': 'Try it now!' + }], + 'advertiser': { + 'description': 'sponsor', + 'domain': 'criteo.com', + 'logo': { 'url': 'https://www.criteo.com/images/criteo-logo.svg', 'height': 300, 'width': 300 } + }, + 'privacy': { + 'optout_click_url': 'https://info.criteo.com/privacy/informations', + 'optout_image_url': 'https://static.criteo.net/flash/icon/nai_small.png', + }, + 'impression_pixels': [{ 'url': 'https://my-impression-pixel/test/impression' }, { 'url': 'https://cas.com/lg.com' }] + }, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + native: {} + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].mediaType).to.equal(NATIVE); + }); + it('should properly parse a bid response with a zoneId', function () { const response = { body: { @@ -2133,6 +2537,163 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].height).to.equal(90); }); + it('should properly parse a bid response with FLEDGE auction configs', function () { + let auctionConfig1 = { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + perBuyerTimeout: { + '*': 500, + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + '*': 60, + 'buyer1': 300, + 'buyer2': 400 + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + foo2: 'bar2', + floor: 1, + currency: 'USD', + perBuyerTimeout: { + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + 'buyer1': 300, + 'buyer2': 400 + }, + }, + sellerCurrency: 'USD', + }; + let auctionConfig2 = { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + perBuyerTimeout: { + '*': 500, + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + '*': 60, + 'buyer1': 300, + 'buyer2': 400 + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + floor: 1, + perBuyerTimeout: { + 'buyer1': 100, + 'buyer2': 200 + }, + perBuyerGroupLimits: { + 'buyer1': 300, + 'buyer2': 400 + }, + }, + sellerCurrency: '???' + }; + const response = { + body: { + ext: { + igi: [{ + impid: 'test-bidId', + igs: [{ + impid: 'test-bidId', + bidId: 'test-bidId', + config: auctionConfig1 + }] + }, { + impid: 'test-bidId-2', + igs: [{ + impid: 'test-bidId-2', + bidId: 'test-bidId-2', + config: auctionConfig2 + }] + }] + }, + }, + }; + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'test-bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + { + bidId: 'test-bidId-2', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const interpretedResponse = spec.interpretResponse(response, request); + expect(interpretedResponse).to.have.property('bids'); + expect(interpretedResponse).to.have.property('fledgeAuctionConfigs'); + expect(interpretedResponse.bids).to.have.lengthOf(0); + expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(2); + expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ + bidId: 'test-bidId', + impid: 'test-bidId', + config: auctionConfig1, + }); + expect(interpretedResponse.fledgeAuctionConfigs[1]).to.deep.equal({ + bidId: 'test-bidId-2', + impid: 'test-bidId-2', + config: auctionConfig2, + }); + }); + [{ hasBidResponseLevelPafData: true, hasBidResponseBidLevelPafData: true, diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aaf63873d93..975271738e5 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -52,17 +52,21 @@ describe('CriteoId module', function () { }); const storageTestCases = [ - { cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, - { cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, - { cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, - { cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: undefined, localStorage: 'bidId2', expected: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId2' }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: undefined, expected: undefined }, ] - storageTestCases.forEach(testCase => it('getId() should return the bidId when it exists in local storages', function () { + storageTestCases.forEach(testCase => it('getId() should return the user id depending on the storage type enabled and the data available', function () { getCookieStub.withArgs('cto_bidid').returns(testCase.cookie); getLocalStorageStub.withArgs('cto_bidid').returns(testCase.localStorage); - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(testCase.submoduleConfig); expect(result.id).to.be.deep.equal(testCase.expected ? { criteoId: testCase.expected } : undefined); expect(result.callback).to.be.a('function'); })) @@ -95,22 +99,24 @@ describe('CriteoId module', function () { }); const responses = [ - { bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, - { bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, shouldWriteCookie: true, shouldWriteLocalStorage: false, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, shouldWriteCookie: false, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, ] responses.forEach(response => describe('test user sync response behavior', function () { const expirationTs = new Date(nowTimestamp + cookiesMaxAge).toString(); it('should save bidId if it exists', function () { - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(response.submoduleConfig); result.callback((id) => { expect(id).to.be.deep.equal(response.bidId ? { criteoId: response.bidId } : undefined); }); @@ -127,16 +133,35 @@ describe('CriteoId module', function () { expect(setCookieStub.calledWith('cto_bundle')).to.be.false; expect(setLocalStorageStub.calledWith('cto_bundle')).to.be.false; } else if (response.bundle) { - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.false; + } + + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.false; + } expect(triggerPixelStub.called).to.be.false; } if (response.bidId) { - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.false; + } + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.false; + } } else { expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.com')).to.be.true; expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.testdev.com')).to.be.true; diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 88c640e38cc..fa44b7daa7a 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -10,10 +10,12 @@ import { addBidResponseHook, currencySupportEnabled, currencyRates, - ready + responseReady } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; import CONSTANTS from '../../../src/constants.json'; +import {server} from '../../mocks/xhr.js'; +import * as events from 'src/events.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -30,12 +32,10 @@ describe('currency', function () { } beforeEach(function () { - fakeCurrencyFileServer = sinon.fakeServer.create(); - ready.reset(); + fakeCurrencyFileServer = server; }); afterEach(function () { - fakeCurrencyFileServer.restore(); setConfig({}); }); @@ -259,6 +259,19 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); }); + it('does not block auctions if rates do not need to be fetched', () => { + sandbox.stub(responseReady, 'resolve'); + setConfig({ + adServerCurrency: 'USD', + rates: { + USD: { + JPY: 100 + } + } + }); + sinon.assert.called(responseReady.resolve); + }) + it('uses rates specified in json when provided and consider boosted bid', function () { setConfig({ adServerCurrency: 'USD', @@ -287,32 +300,56 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('1000.000'); }); - it('uses default rates when currency file fails to load', function () { - setConfig({}); - - setConfig({ - adServerCurrency: 'USD', - defaultRates: { - USD: { - JPY: 100 + describe('when rates fail to load', () => { + let bid, addBidResponse, reject; + beforeEach(() => { + bid = makeBid({cpm: 100, currency: 'JPY', bidder: 'rubicoin'}); + addBidResponse = sinon.spy(); + reject = sinon.spy(); + }) + it('uses default rates if specified', function () { + setConfig({ + adServerCurrency: 'USD', + defaultRates: { + USD: { + JPY: 100 + } } - } - }); - - // default response is 404 - fakeCurrencyFileServer.respond(); + }); - var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; - var innerBid; + // default response is 404 + addBidResponseHook(addBidResponse, 'au', bid); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', sinon.match(innerBid => { + expect(innerBid.cpm).to.equal('1.0000'); + expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); + expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); + return true; + })); + }); - addBidResponseHook(function(adCodeId, bid) { - innerBid = bid; - }, 'elementId', bid); + it('rejects bids if no default rates are specified', () => { + setConfig({ + adServerCurrency: 'USD', + }); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }); - expect(innerBid.cpm).to.equal('1.0000'); - expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); - expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); - }); + it('attempts to load rates again on the next auction', () => { + setConfig({ + adServerCurrency: 'USD', + }); + fakeCurrencyFileServer.respond(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {}); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', bid, reject); + }) + }) }); describe('currency.addBidResponseDecorator bidResponseQueue', function () { @@ -321,29 +358,26 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var bid = { 'cpm': 1, 'currency': 'USD' }; + const bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - var marker = false; - let promiseResolved = false; + let responseAdded = false; + let isReady = false; + responseReady.promise.then(() => { isReady = true }); + addBidResponseHook(Object.assign(function() { - marker = true; - }, { - bail: function (promise) { - promise.then(() => promiseResolved = true); - } + responseAdded = true; }), 'elementId', bid); - expect(marker).to.equal(false); - setTimeout(() => { - expect(promiseResolved).to.be.false; + expect(responseAdded).to.equal(false); + expect(isReady).to.equal(false); fakeCurrencyFileServer.respond(); setTimeout(() => { - expect(marker).to.equal(true); - expect(promiseResolved).to.be.true; + expect(responseAdded).to.equal(true); + expect(isReady).to.equal(true); done(); }); }); @@ -419,6 +453,23 @@ describe('currency', function () { expect(reject.calledOnce).to.be.true; }); + it('should reject bid when rates have not loaded when the auction times out', () => { + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({'adServerCurrency': 'JPY'}); + const bid = makeBid({cpm: 1, currency: 'USD', auctionId: 'aid'}); + const noConversionBid = makeBid({cpm: 1, currency: 'JPY', auctionId: 'aid'}); + const reject = sinon.spy(); + const addBidResponse = sinon.spy(); + addBidResponseHook(addBidResponse, 'au', bid, reject); + addBidResponseHook(addBidResponse, 'au', noConversionBid, reject); + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, {auctionId: 'aid'}); + fakeCurrencyFileServer.respond(); + sinon.assert.calledOnce(addBidResponse); + sinon.assert.calledWith(addBidResponse, 'au', noConversionBid, reject); + sinon.assert.calledOnce(reject); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }) + it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 88c54212aff..8eedcdb4a07 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -293,4 +293,51 @@ describe('C-WIRE bid adapter', () => { expect(bids[0].ad).to.exist; }) }); + + describe('add user-syncs', function () { + it('empty user-syncs if no consent given', function () { + const userSyncs = spec.getUserSyncs({}, {}, {}, {}); + + expect(userSyncs).to.be.empty + }) + it('empty user-syncs if no syncOption enabled', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + const userSyncs = spec.getUserSyncs({}, {}, gdprConsent, {}); + + expect(userSyncs).to.be.empty + }) + + it('user-syncs with enabled pixel option', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + let synOptions = {pixelEnabled: true, iframeEnabled: true}; + const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); + + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'); + }) + + it('user-syncs with enabled iframe option', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + let synOptions = {iframeEnabled: true}; + const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); + + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'); + }) + }) }); diff --git a/test/spec/modules/debugging_mod_spec.js b/test/spec/modules/debugging_mod_spec.js index 8c7f0e84bce..ab99ba2aa0c 100644 --- a/test/spec/modules/debugging_mod_spec.js +++ b/test/spec/modules/debugging_mod_spec.js @@ -103,8 +103,8 @@ describe('bid interceptor', () => { }); describe('rule', () => { - function matchingRule({replace, options}) { - setRules({when: {}, then: replace, options: options}); + function matchingRule({replace, options, paapi}) { + setRules({when: {}, then: replace, options: options, paapi}); return interceptor.match({}); } @@ -164,6 +164,24 @@ describe('bid interceptor', () => { }); }); + describe('paapi', () => { + it('should accept literals', () => { + const mockConfig = [ + {paapi: 1}, + {paapi: 2} + ] + const paapi = matchingRule({paapi: mockConfig}).paapi({}); + expect(paapi).to.eql(mockConfig); + }); + + it('should accept a function and pass extra args to it', () => { + const paapiDef = sinon.stub(); + const args = [{}, {}, {}]; + matchingRule({paapi: paapiDef}).paapi(...args); + expect(paapiDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }) + }) + describe('.options', () => { it('should include default rule options', () => { const optDef = {someOption: 'value'}; @@ -181,16 +199,17 @@ describe('bid interceptor', () => { }); describe('intercept()', () => { - let done, addBid; + let done, addBid, addPaapiConfig; function intercept(args = {}) { const bidRequest = {bids: args.bids || []}; - return interceptor.intercept(Object.assign({bidRequest, done, addBid}, args)); + return interceptor.intercept(Object.assign({bidRequest, done, addBid, addPaapiConfig}, args)); } beforeEach(() => { done = sinon.spy(); addBid = sinon.spy(); + addPaapiConfig = sinon.spy(); }); describe('on no match', () => { @@ -253,6 +272,29 @@ describe('bid interceptor', () => { }); }); + it('should call addPaapiConfigs when provided', () => { + const mockPaapiConfigs = [ + {paapi: 1}, + {paapi: 2} + ] + setRules({ + when: {id: 2}, + paapi: mockPaapiConfigs, + }); + intercept({bidRequest: REQUEST}); + expect(addPaapiConfig.callCount).to.eql(2); + mockPaapiConfigs.forEach(cfg => sinon.assert.calledWith(addPaapiConfig, cfg)) + }) + + it('should not call onBid when then is null', () => { + setRules({ + when: {id: 2}, + then: null + }); + intercept({bidRequest: REQUEST}); + sinon.assert.notCalled(addBid); + }) + it('should call done()', () => { intercept({bidRequest: REQUEST}); expect(done.calledOnce).to.be.true; diff --git a/test/spec/modules/deepintentBidAdapter_spec.js b/test/spec/modules/deepintentBidAdapter_spec.js index d2a351b4089..644e9255789 100644 --- a/test/spec/modules/deepintentBidAdapter_spec.js +++ b/test/spec/modules/deepintentBidAdapter_spec.js @@ -357,5 +357,34 @@ describe('Deepintent adapter', function () { let response = spec.interpretResponse(invalidResponse, bRequest); expect(response[0].mediaType).to.equal(undefined); }); - }) + }); + describe('GPP and coppa', function() { + it('Request params check with GPP Consent', function () { + let bidderReq = {gppConsent: {gppString: 'gpp-string-test', applicableSections: [5]}}; + let bRequest = spec.buildRequests(request, bidderReq); + let data = JSON.parse(bRequest.data); + expect(data.regs.gpp).to.equal('gpp-string-test'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + it('Request params check with GPP Consent read from ortb2', function () { + let bidderReq = { + ortb2: { + regs: { + gpp: 'gpp-test-string', + gpp_sid: [5] + } + } + }; + let bRequest = spec.buildRequests(request, bidderReq); + let data = JSON.parse(bRequest.data); + expect(data.regs.gpp).to.equal('gpp-test-string'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + it('should include coppa flag in bid request if coppa is set to true', () => { + let bidderReq = {ortb2: {regs: {coppa: 1}}}; + let bRequest = spec.buildRequests(request, bidderReq); + let data = JSON.parse(bRequest.data); + expect(data.regs.coppa).to.equal(1); + }); + }); }); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 89485adf28b..39713c2b51a 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -1,43 +1,61 @@ -import { expect } from 'chai'; +import {expect} from 'chai'; import parse from 'url-parse'; -import {buildDfpVideoUrl, buildAdpodVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; -import adUnit from 'test/fixtures/video/adUnit.json'; +import {buildAdpodVideoUrl, buildDfpVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; +import AD_UNIT from 'test/fixtures/video/adUnit.json'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; -import { targeting } from 'src/targeting.js'; -import { auctionManager } from 'src/auctionManager.js'; -import { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; +import {deepClone} from 'src/utils.js'; +import {config} from 'src/config.js'; +import {targeting} from 'src/targeting.js'; +import {auctionManager} from 'src/auctionManager.js'; +import {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; -import { server } from 'test/mocks/xhr.js'; +import {server} from 'test/mocks/xhr.js'; import * as adServer from 'src/adserver.js'; -import {deepClone} from 'src/utils.js'; import {hook} from '../../../src/hook.js'; -import {getRefererInfo} from '../../../src/refererDetection.js'; - -const bid = { - videoCacheKey: 'abc', - adserverTargeting: { - hb_uuid: 'abc', - hb_cache_id: 'abc', - }, -}; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {AuctionIndex} from '../../../src/auctionIndex.js'; describe('The DFP video support module', function () { before(() => { hook.ready(); }); - let sandbox; + let sandbox, bid, adUnit; beforeEach(() => { sandbox = sinon.sandbox.create(); + bid = { + videoCacheKey: 'abc', + adserverTargeting: { + hb_uuid: 'abc', + hb_cache_id: 'abc', + }, + }; + adUnit = deepClone(AD_UNIT); }); afterEach(() => { sandbox.restore(); }); + function getURL(options) { + return parse(buildDfpVideoUrl(Object.assign({ + adUnit: adUnit, + bid: bid, + params: { + 'iu': 'my/adUnit' + } + }, options))) + } + function getQueryParams(options) { + return utils.parseQS(getURL(options).query); + } + + function getCustomParams(options) { + return utils.parseQS('?' + decodeURIComponent(getQueryParams(options).cust_params)); + } + Object.entries({ params: { params: { @@ -51,27 +69,25 @@ describe('The DFP video support module', function () { describe(`when using ${t}`, () => { it('should use page location as default for description_url', () => { sandbox.stub(dep, 'ri').callsFake(() => ({page: 'example.com'})); - - const url = parse(buildDfpVideoUrl(Object.assign({ - adUnit: adUnit, - bid: bid, - }, options))); - const prm = utils.parseQS(url.query); + const prm = getQueryParams(options); expect(prm.description_url).to.eql('example.com'); - }) - }) + }); + + it('should use a URI encoded page location as default for description_url', () => { + sandbox.stub(dep, 'ri').callsFake(() => ({page: 'https://example.com?iu=/99999999/news&cust_params=current_hour%3D12%26newscat%3Dtravel&pbjs_debug=true'})); + const prm = getQueryParams(options); + expect(prm.description_url).to.eql('https%3A%2F%2Fexample.com%3Fiu%3D%2F99999999%2Fnews%26cust_params%3Dcurrent_hour%253D12%2526newscat%253Dtravel%26pbjs_debug%3Dtrue'); + }); + }); }) it('should make a legal request URL when given the required params', function () { - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bid, + const url = getURL({ params: { 'iu': 'my/adUnit', 'description_url': 'someUrl.com', } - })); - + }) expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); @@ -88,15 +104,10 @@ describe('The DFP video support module', function () { }); it('can take an adserver url as a parameter', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.vastUrl = 'vastUrl.example'; - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, + bid.vastUrl = 'vastUrl.example'; + const url = getURL({ url: 'https://video.adserver.example/', - })); - + }) expect(url.host).to.equal('video.adserver.example'); }); @@ -110,161 +121,64 @@ describe('The DFP video support module', function () { }); it('overwrites url params when both url and params object are given', function () { - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bid, + const params = getQueryParams({ url: 'https://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s', params: { iu: 'my/adUnit' } - })); + }); - const queryObject = utils.parseQS(url.query); - expect(queryObject.iu).to.equal('my/adUnit'); + expect(params.iu).to.equal('my/adUnit'); }); it('should override param defaults with user-provided ones', function () { - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bid, + const params = getQueryParams({ params: { - 'iu': 'my/adUnit', 'output': 'vast', } - })); - - expect(utils.parseQS(url.query)).to.have.property('output', 'vast'); + }); + expect(params.output).to.equal('vast'); }); it('should include the cache key and adserver targeting in cust_params', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + bid.adserverTargeting = Object.assign(bid.adserverTargeting, { hb_adid: 'ad_id', }); - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); - const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const customParams = getCustomParams() expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); }); - it('should include the us_privacy key when USP Consent is available', function () { - let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); - uspDataHandlerStub.returns('1YYY'); - - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); - expect(queryObject.us_privacy).to.equal('1YYY'); - uspDataHandlerStub.restore(); - }); - - it('should not include the us_privacy key when USP Consent is not available', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); - expect(queryObject.us_privacy).to.equal(undefined); - }); - it('should include the GDPR keys when GDPR Consent is available', function () { - let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - gdprDataHandlerStub.returns({ + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ gdprApplies: true, consentString: 'consent', addtlConsent: 'moreConsent' }); - - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); + const queryObject = getQueryParams(); expect(queryObject.gdpr).to.equal('1'); expect(queryObject.gdpr_consent).to.equal('consent'); expect(queryObject.addtl_consent).to.equal('moreConsent'); - gdprDataHandlerStub.restore(); }); it('should not include the GDPR keys when GDPR Consent is not available', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); + const queryObject = getQueryParams() expect(queryObject.gdpr).to.equal(undefined); expect(queryObject.gdpr_consent).to.equal(undefined); expect(queryObject.addtl_consent).to.equal(undefined); }); it('should only include the GDPR keys for GDPR Consent fields with values', function () { - let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - gdprDataHandlerStub.returns({ + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ gdprApplies: true, consentString: 'consent', }); - - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); + const queryObject = getQueryParams() expect(queryObject.gdpr).to.equal('1'); expect(queryObject.gdpr_consent).to.equal('consent'); expect(queryObject.addtl_consent).to.equal(undefined); - gdprDataHandlerStub.restore(); }); - describe('GAM PPID', () => { let ppid; let getPPIDStub; @@ -280,29 +194,283 @@ describe('The DFP video support module', function () { 'url': {url: 'https://video.adserver.mock/', params: {'iu': 'mock/unit'}} }).forEach(([t, opts]) => { describe(`when using ${t}`, () => { - function buildUrlAndGetParams() { - const url = parse(buildDfpVideoUrl(Object.assign({ - adUnit: adUnit, - bid: deepClone(bid), - }, opts))); - return utils.parseQS(url.query); - } - it('should be included if available', () => { ppid = 'mockPPID'; - const q = buildUrlAndGetParams(); + const q = getQueryParams(opts); expect(q.ppid).to.equal('mockPPID'); }); it('should not be included if not available', () => { ppid = undefined; - const q = buildUrlAndGetParams(); + const q = getQueryParams(opts); expect(q.hasOwnProperty('ppid')).to.be.false; }) }) }) }) + describe('ORTB video parameters', () => { + Object.entries({ + plcmt: [ + { + video: { + plcmt: 1 + }, + expected: '1' + } + ], + min_ad_duration: [ + { + video: { + minduration: 123 + }, + expected: '123000' + } + ], + max_ad_duration: [ + { + video: { + maxduration: 321 + }, + expected: '321000' + } + ], + vpos: [ + { + video: { + startdelay: 0 + }, + expected: 'preroll' + }, + { + video: { + startdelay: -1 + }, + expected: 'midroll' + }, + { + video: { + startdelay: -2 + }, + expected: 'postroll' + }, + { + video: { + startdelay: 10 + }, + expected: 'midroll' + } + ], + vconp: [ + { + video: { + playbackmethod: [7] + }, + expected: '2' + }, + { + video: { + playbackmethod: [7, 1] + }, + expected: undefined + } + ], + vpa: [ + { + video: { + playbackmethod: [1, 2, 4, 5, 6, 7] + }, + expected: 'auto' + }, + { + video: { + playbackmethod: [3, 7], + }, + expected: 'click' + }, + { + video: { + playbackmethod: [1, 3], + }, + expected: undefined + } + ], + vpmute: [ + { + video: { + playbackmethod: [1, 3, 4, 5, 7] + }, + expected: '0' + }, + { + video: { + playbackmethod: [2, 6, 7], + }, + expected: '1' + }, + { + video: { + playbackmethod: [1, 2] + }, + expected: undefined + } + ] + }).forEach(([param, cases]) => { + describe(param, () => { + cases.forEach(({video, expected}) => { + describe(`when mediaTypes.video has ${JSON.stringify(video)}`, () => { + it(`fills in ${param} = ${expected}`, () => { + Object.assign(adUnit.mediaTypes.video, video); + expect(getQueryParams()[param]).to.eql(expected); + }); + it(`does not override pub-provided params.${param}`, () => { + Object.assign(adUnit.mediaTypes.video, video); + expect(getQueryParams({ + params: { + [param]: 'OG' + } + })[param]).to.eql('OG'); + }); + it('does not fill if param has no value', () => { + expect(getQueryParams().hasOwnProperty(param)).to.be.false; + }) + }) + }) + }) + }) + }); + + describe('ppsj', () => { + let ortb2; + beforeEach(() => { + ortb2 = null; + }) + + function getSignals() { + const ppsj = JSON.parse(atob(getQueryParams().ppsj)); + return Object.fromEntries(ppsj.PublisherProvidedTaxonomySignals.map(sig => [sig.taxonomy, sig.values])); + } + + Object.entries({ + 'FPD from bid request'() { + bid.requestId = 'req-id'; + sandbox.stub(auctionManager, 'index').get(() => stubAuctionIndex({ + bidRequests: [ + { + bidId: 'req-id', + ortb2 + } + ] + })); + }, + 'global FPD from auction'() { + bid.auctionId = 'auid'; + sandbox.stub(auctionManager, 'index').get(() => new AuctionIndex(() => [{ + getAuctionId: () => 'auid', + getFPD: () => ({ + global: ortb2 + }) + }])); + } + }).forEach(([t, setup]) => { + describe(`using ${t}`, () => { + beforeEach(setup); + it('does not fill if there\'s no segments in segtax 4 or 6', () => { + ortb2 = { + site: { + content: { + data: [ + { + segment: [ + {id: '1'}, + {id: '2'} + ] + }, + ] + } + }, + user: { + data: [ + { + ext: { + segtax: 1, + }, + segment: [ + {id: '3'} + ] + } + ] + } + } + expect(getQueryParams().ppsj).to.not.exist; + }); + + const SEGMENTS = [ + { + ext: { + segtax: 4, + }, + segment: [ + {id: '4-1'}, + {id: '4-2'} + ] + }, + { + ext: { + segtax: 4, + }, + segment: [ + {id: '4-2'}, + {id: '4-3'} + ] + }, + { + ext: { + segtax: 6, + }, + segment: [ + {id: '6-1'}, + {id: '6-2'} + ] + }, + { + ext: { + segtax: 6, + }, + segment: [ + {id: '6-2'}, + {id: '6-3'} + ] + }, + ] + + it('collects user.data segments with segtax = 4 into IAB_AUDIENCE_1_1', () => { + ortb2 = { + user: { + data: SEGMENTS + } + } + expect(getSignals()).to.eql({ + IAB_AUDIENCE_1_1: ['4-1', '4-2', '4-3'] + }) + }) + + it('collects site.content.data segments with segtax = 6 into IAB_CONTENT_2_2', () => { + ortb2 = { + site: { + content: { + data: SEGMENTS + } + } + } + expect(getSignals()).to.eql({ + IAB_CONTENT_2_2: ['6-1', '6-2', '6-3'] + }) + }) + }) + }) + }) + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', @@ -629,7 +797,6 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); - expect(queryParams).to.have.property('us_privacy', '1YYY'); expect(queryParams).to.have.property('gdpr', '1'); expect(queryParams).to.have.property('gdpr_consent', 'consent'); expect(queryParams).to.have.property('addtl_consent', 'moreConsent'); diff --git a/test/spec/modules/dgkeywordRtdProvider_spec.js b/test/spec/modules/dgkeywordRtdProvider_spec.js index 754740b7a64..ff88ea0512f 100644 --- a/test/spec/modules/dgkeywordRtdProvider_spec.js +++ b/test/spec/modules/dgkeywordRtdProvider_spec.js @@ -91,6 +91,22 @@ describe('Digital Garage Keyword Module', function () { expect(dgRtd.getTargetBidderOfDgKeywords(adUnits_no_target)).an('array') .that.is.empty; }); + it('convertKeywordsToString method unit test', function () { + const keywordsTest = [ + { keywords: { param1: 'keywords1' }, result: 'param1=keywords1' }, + { keywords: { param1: 'keywords1', param2: 'keywords2' }, result: 'param1=keywords1,param2=keywords2' }, + { keywords: { p1: 'k1', p2: 'k2', p: 'k' }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: 'k2', p: ['k'] }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k1,p2=k21,p2=k22,p=k' }, + { keywords: { p1: ['k11', 'k12', 'k13'], p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k11,p1=k12,p1=k13,p2=k21,p2=k22,p=k' }, + { keywords: { p1: [], p2: ['', ''], p: [''] }, result: 'p1,p2,p' }, + { keywords: { p1: 1, p2: [1, 'k2'], p: '' }, result: 'p1,p2=k2,p' }, + { keywords: { p1: ['k1', 2, 'k3'], p2: [1, 2], p: 3 }, result: 'p1=k1,p1=k3,p2,p' }, + ]; + for (const test of keywordsTest) { + expect(dgRtd.convertKeywordsToString(test.keywords)).equal(test.result); + } + }) it('should have targets', function () { const adUnits_targets = [ { @@ -242,16 +258,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -275,16 +291,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -318,16 +334,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[1].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[0].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].ortb2Imp).to.be.an('undefined'); if (!IGNORE_SET_ORTB2) { expect(pbjs.getBidderConfig()).to.be.deep.equal({ diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index 078add73046..d148d5062a4 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -1,5 +1,18 @@ import { expect } from 'chai'; -import { spec } from 'modules/discoveryBidAdapter.js'; +import { + spec, + getPmgUID, + storage, + getPageTitle, + getPageDescription, + getPageKeywords, + getConnectionDownLink, + THIRD_PARTY_COOKIE_ORIGIN, + COOKIE_KEY_MGUID, + getCurrentTimeToUTCString, + buildUTMTagData +} from 'modules/discoveryBidAdapter.js'; +import * as utils from 'src/utils.js'; describe('discovery:BidAdapterTests', function () { let bidRequestData = { @@ -11,12 +24,59 @@ describe('discovery:BidAdapterTests', function () { bidder: 'discovery', params: { token: 'd0f4902b616cc5c38cbe0a08676d0ed9', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://discovery.popin.cc', + }, + refererInfo: { + page: 'https://discovery.popin.cc', + stack: [ + 'a.com', + 'b.com' + ] }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + CxSegments: [] + } + } + }, + site: { + domain: 'discovery.popin.cc', + publisher: { + domain: 'discovery.popin.cc' + }, + page: 'https://discovery.popin.cc', + cat: ['IAB-19', 'IAB-20'], }, }, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + keywords: ['travel', 'sport'], + pbadslot: '202309999' + } + } + }, adUnitCode: 'regular_iframe', transactionId: 'd163f9e2-7ecd-4c2c-a3bd-28ceb52a60ee', sizes: [[300, 250]], @@ -29,9 +89,96 @@ describe('discovery:BidAdapterTests', function () { bidderWinsCount: 0, }, ], + ortb2: { + user: { + data: { + segment: [ + { + id: '412' + } + ], + name: 'test.popin.cc', + ext: { + segclass: '1', + segtax: 503 + } + } + } + } }; let request = []; + let bidRequestDataNoParams = { + bidderCode: 'discovery', + auctionId: 'ff66e39e-4075-4d18-9854-56fde9b879ac', + bidderRequestId: '4fec04e87ad785', + bids: [ + { + bidder: 'discovery', + params: { + referrer: 'https://discovery.popin.cc', + }, + refererInfo: { + page: 'https://discovery.popin.cc', + stack: [ + 'a.com', + 'b.com' + ] + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + CxSegments: [] + } + } + }, + site: { + domain: 'discovery.popin.cc', + publisher: { + domain: 'discovery.popin.cc' + }, + page: 'https://discovery.popin.cc', + cat: ['IAB-19', 'IAB-20'], + }, + }, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + keywords: ['travel', 'sport'], + pbadslot: '202309999' + } + } + }, + adUnitCode: 'regular_iframe', + transactionId: 'd163f9e2-7ecd-4c2c-a3bd-28ceb52a60ee', + sizes: [[300, 250]], + bidId: '276092a19e05eb', + bidderRequestId: '1fadae168708b', + auctionId: 'ff66e39e-4075-4d18-9854-56fde9b879ac', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + }; + it('discovery:validate_pub_params', function () { expect( spec.isBidRequestValid({ @@ -45,11 +192,101 @@ describe('discovery:BidAdapterTests', function () { ).to.equal(true); }); + it('isBidRequestValid:no_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'discovery', + params: {}, + }) + ).to.equal(true); + }); + it('discovery:validate_generated_params', function () { request = spec.buildRequests(bidRequestData.bids, bidRequestData); let req_data = JSON.parse(request.data); expect(req_data.imp).to.have.lengthOf(1); }); + describe('first party data', function () { + it('should pass additional parameter in request for topics', function () { + const request = spec.buildRequests(bidRequestData.bids, bidRequestData); + let res = JSON.parse(request.data); + expect(res.ext.tpData).to.deep.equal(bidRequestData.ortb2.user.data); + }); + }); + + describe('discovery: buildRequests', function() { + describe('getPmgUID function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(utils, 'generateUUID').returns('new-uuid'); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should generate new UUID and set cookie if not exists', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => null); + const uid = getPmgUID(); + expect(uid).to.equal('new-uuid'); + expect(storage.setCookie.calledOnce).to.be.true; + }); + + it('should return existing UUID from cookie', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => 'existing-uuid'); + const uid = getPmgUID(); + expect(uid).to.equal('existing-uuid'); + expect(storage.setCookie.called).to.be.true; + }); + + it('should not set new UUID when cookies are not enabled', () => { + storage.cookiesAreEnabled.callsFake(() => false); + storage.getCookie.callsFake(() => null); + getPmgUID(); + expect(storage.setCookie.calledOnce).to.be.false; + }); + }) + describe('buildUTMTagData function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(utils, 'parseUrl').returns({ + search: { + utm_source: 'example.com' + } + }); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should set UTM cookie', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => null); + buildUTMTagData(); + expect(storage.setCookie.calledOnce).to.be.true; + }); + + it('should not set UTM when cookies are not enabled', () => { + storage.cookiesAreEnabled.callsFake(() => false); + storage.getCookie.callsFake(() => null); + buildUTMTagData(); + expect(storage.setCookie.calledOnce).to.be.false; + }); + }) + }); it('discovery:validate_response_params', function () { let tempAdm = '' @@ -90,4 +327,324 @@ describe('discovery:BidAdapterTests', function () { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); + + describe('discovery: getUserSyncs', function() { + const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html'; + const IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }; + const IFRAME_DISABLED = { + iframeEnabled: false, + pixelEnabled: false, + }; + const GDPR_CONSENT = { + consentString: 'gdprConsentString', + gdprApplies: true + }; + const USP_CONSENT = { + consentString: 'uspConsentString' + } + + let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; + syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; + const expectedIframeSyncs = [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + + it('should return nothing if iframe is disabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.be.undefined; + }); + + it('should do userSyncs if iframe is enabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.deep.equal(expectedIframeSyncs); + }); + }); +}); + +describe('discovery Bid Adapter Tests', function () { + describe('buildRequests', () => { + describe('getPageTitle function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document title if available', function() { + const fakeTopDocument = { + title: 'Top Document Title', + querySelector: () => ({ content: 'Top Document Title test' }) + }; + const fakeTopWindow = { + document: fakeTopDocument + }; + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the content of top og:title meta tag if title is empty', function() { + const ogTitleContent = 'Top OG Title Content'; + const fakeTopWindow = { + document: { + title: '', + querySelector: sandbox.stub().withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }) + } + }; + + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return the document title if no og:title meta tag is present', function() { + document.title = 'Test Page Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + + const result = getPageTitle({ top: undefined }); + expect(result).to.equal('Test Page Title'); + }); + + it('should return the content of og:title meta tag if present', function() { + document.title = ''; + const ogTitleContent = 'Top OG Title Content'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return an empty string if no title or og:title meta tag is found', function() { + document.title = ''; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(''); + }); + + it('should handle exceptions when accessing top.document and fallback to current document', function() { + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const ogTitleContent = 'Current OG Title Content'; + document.title = 'Current Document Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle(fakeWindow); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document description if available', function() { + const descriptionContent = 'Top Document Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[name="description"]').returns({ content: descriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(descriptionContent); + }); + + it('should return the top document og:description if description is not present', function() { + const ogDescriptionContent = 'Top OG Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(ogDescriptionContent); + }); + + it('should return the current document description if top document is not accessible', function() { + const descriptionContent = 'Current Document Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="description"]').returns({ content: descriptionContent }) + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(descriptionContent); + }); + + it('should return the current document og:description if description is not present and top document is not accessible', function() { + const ogDescriptionContent = 'Current OG Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }); + + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(ogDescriptionContent); + }); + }); + + describe('getPageKeywords function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document keywords if available', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + const fakeTopDocument = { + querySelector: sandbox.stub() + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + + const result = getPageKeywords({ top: fakeTopWindow }); + expect(result).to.equal(keywordsContent); + }); + + it('should return the current document keywords if top document is not accessible', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }); + + // æ¨Ąæ‹ŸéĄļåą‚įĒ—åŖčŽŋ问åŧ‚常 + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + + const result = getPageKeywords(fakeWindow); + expect(result).to.equal(keywordsContent); + }); + + it('should return an empty string if no keywords meta tag is found', function() { + sandbox.stub(document, 'querySelector').withArgs('meta[name="keywords"]').returns(null); + + const result = getPageKeywords(); + expect(result).to.equal(''); + }); + }); + describe('getConnectionDownLink function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the downlink value as a string if available', function() { + const downlinkValue = 2.5; + const fakeNavigator = { + connection: { + downlink: downlinkValue + } + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.equal(downlinkValue.toString()); + }); + + it('should return undefined if downlink is not available', function() { + const fakeNavigator = { + connection: {} + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should return undefined if connection is not available', function() { + const fakeNavigator = {}; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should handle cases where navigator is not defined', function() { + const result = getConnectionDownLink({}); + expect(result).to.be.undefined; + }); + }); + + describe('getUserSyncs with message event listener', function() { + function messageHandler(event) { + if (!event.data || event.origin !== THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + window.removeEventListener('message', messageHandler, true); + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + } + + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(window, 'removeEventListener'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should set a cookie when a valid message is received', () => { + const fakeEvent = { + data: { optout: '', mguid: '12345' }, + origin: THIRD_PARTY_COOKIE_ORIGIN, + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.calledOnce).to.be.true; + expect(window.removeEventListener.calledWith('message', messageHandler, true)).to.be.true; + expect(storage.setCookie.calledWith(COOKIE_KEY_MGUID, '12345', sinon.match.string)).to.be.true; + }); + it('should not do anything when an invalid message is received', () => { + const fakeEvent = { + data: null, + origin: 'http://invalid-origin.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.notCalled).to.be.true; + expect(window.removeEventListener.notCalled).to.be.true; + expect(storage.setCookie.notCalled).to.be.true; + }); + }); + }); }); diff --git a/test/spec/modules/dmdIdSystem_spec.js b/test/spec/modules/dmdIdSystem_spec.js index 3096a8e55f5..16c32f184a3 100644 --- a/test/spec/modules/dmdIdSystem_spec.js +++ b/test/spec/modules/dmdIdSystem_spec.js @@ -60,7 +60,7 @@ describe('Dmd ID System', function () { it('Should invoke callback with response from API call', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; @@ -73,7 +73,7 @@ describe('Dmd ID System', function () { it('Should log error if API response is not valid', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; diff --git a/test/spec/modules/docereeAdManagerBidAdapter_spec.js b/test/spec/modules/docereeAdManagerBidAdapter_spec.js new file mode 100644 index 00000000000..26b054f4e29 --- /dev/null +++ b/test/spec/modules/docereeAdManagerBidAdapter_spec.js @@ -0,0 +1,125 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/docereeAdManagerBidAdapter.js'; +import { config } from '../../../src/config.js'; + +describe('docereeadmanager', function () { + config.setConfig({ + docereeadmanager: { + user: { + data: { + email: '', + firstname: '', + lastname: '', + mobile: '', + specialization: '', + organization: '', + hcpid: '', + dob: '', + gender: '', + city: '', + state: '', + country: '', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '', + zipcode: '', + userconsent: '', + }, + }, + }, + }); + let bid = { + bidId: 'testing', + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', + gdpr: '1', + gdprconsent: + 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', + }, + }; + + describe('isBidRequestValid', function () { + it('Should return true if placementId is present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if placementId is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('isGdprConsentPresent', function () { + it('Should return true if gdpr consent is present', function () { + expect(spec.isGdprConsentPresent(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + serverRequest = serverRequest[0]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + 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://dai.doceree.com/drs/quest'); + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: { + cpm: 3.576, + currency: 'USD', + width: 250, + height: 300, + ad: '

I am an ad

', + ttl: 30, + creativeId: 'div-1', + netRevenue: false, + bidderCode: '123', + dealId: 232, + requestId: '123', + meta: { + brandId: null, + advertiserDomains: ['https://dai.doceree.com/drs/quest'], + }, + }, + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys( + 'requestId', + 'cpm', + 'width', + 'height', + 'ad', + 'ttl', + 'netRevenue', + 'currency', + 'mediaType', + 'creativeId', + 'meta' + ); + expect(dataItem.requestId).to.equal('123'); + expect(dataItem.cpm).to.equal(3.576); + expect(dataItem.width).to.equal(250); + expect(dataItem.height).to.equal(300); + expect(dataItem.ad).to.equal('

I am an ad

'); + expect(dataItem.ttl).to.equal(30); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.creativeId).to.equal('div-1'); + expect(dataItem.meta.advertiserDomains).to.be.an('array').that.is.not + .empty; + }); + }); +}); diff --git a/test/spec/modules/docereeBidAdapter_spec.js b/test/spec/modules/docereeBidAdapter_spec.js index dadbb56b0c0..25da8b256fc 100644 --- a/test/spec/modules/docereeBidAdapter_spec.js +++ b/test/spec/modules/docereeBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/docereeBidAdapter.js'; import { config } from '../../../src/config.js'; +import * as utils from 'src/utils.js'; describe('BidlabBidAdapter', function () { config.setConfig({ @@ -102,4 +103,36 @@ describe('BidlabBidAdapter', function () { expect(dataItem.meta.advertiserDomains[0]).to.equal('doceree.com') }); }) + describe('onBidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon([]); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); }); diff --git a/test/spec/modules/dsaControl_spec.js b/test/spec/modules/dsaControl_spec.js new file mode 100644 index 00000000000..0d7c52b5efd --- /dev/null +++ b/test/spec/modules/dsaControl_spec.js @@ -0,0 +1,113 @@ +import {addBidResponseHook, setMetaDsa, reset} from '../../../modules/dsaControl.js'; +import CONSTANTS from 'src/constants.json'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {AuctionIndex} from '../../../src/auctionIndex.js'; + +describe('DSA transparency', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + reset(); + }); + + describe('addBidResponseHook', () => { + const auctionId = 'auction-id'; + let bid, auction, fpd, next, reject; + beforeEach(() => { + next = sinon.stub(); + reject = sinon.stub(); + fpd = {}; + bid = { + auctionId + } + auction = { + getAuctionId: () => auctionId, + getFPD: () => ({global: fpd}) + } + sandbox.stub(auctionManager, 'index').get(() => new AuctionIndex(() => [auction])); + }); + + function expectRejection(reason) { + addBidResponseHook(next, 'adUnit', bid, reject); + sinon.assert.calledWith(reject, reason); + sinon.assert.notCalled(next); + } + + function expectAcceptance() { + addBidResponseHook(next, 'adUnit', bid, reject); + sinon.assert.notCalled(reject); + sinon.assert.calledWith(next, 'adUnit', bid, reject); + } + + [2, 3].forEach(required => { + describe(`when regs.ext.dsa.dsarequired is ${required} (required)`, () => { + beforeEach(() => { + fpd = { + regs: {ext: {dsa: {dsarequired: required}}} + }; + }); + + it('should reject bids that have no meta.dsa', () => { + expectRejection(CONSTANTS.REJECTION_REASON.DSA_REQUIRED); + }); + + it('should accept bids that do', () => { + bid.meta = {dsa: {}}; + expectAcceptance(); + }); + + describe('and pubrender = 0 (rendering by publisher not supported)', () => { + beforeEach(() => { + fpd.regs.ext.dsa.pubrender = 0; + }); + + it('should reject bids with adrender = 0 (advertiser will not render)', () => { + bid.meta = {dsa: {adrender: 0}}; + expectRejection(CONSTANTS.REJECTION_REASON.DSA_MISMATCH); + }); + + it('should accept bids with adrender = 1 (advertiser will render)', () => { + bid.meta = {dsa: {adrender: 1}}; + expectAcceptance(); + }); + }); + describe('and pubrender = 2 (publisher will render)', () => { + beforeEach(() => { + fpd.regs.ext.dsa.pubrender = 2; + }); + + it('should reject bids with adrender = 1 (advertiser will render)', () => { + bid.meta = {dsa: {adrender: 1}}; + expectRejection(CONSTANTS.REJECTION_REASON.DSA_MISMATCH); + }); + + it('should accept bids with adrender = 0 (advertiser will not render)', () => { + bid.meta = {dsa: {adrender: 0}}; + expectAcceptance(); + }) + }) + }); + }); + [undefined, 'garbage', 0, 1].forEach(required => { + describe(`when regs.ext.dsa.dsarequired is ${required}`, () => { + beforeEach(() => { + if (required != null) { + fpd = { + regs: {ext: {dsa: {dsarequired: required}}} + } + } + }); + + it('should accept bids regardless of their meta.dsa', () => { + addBidResponseHook(next, 'adUnit', bid, reject); + sinon.assert.notCalled(reject); + sinon.assert.calledWith(next, 'adUnit', bid, reject); + }) + }) + }) + it('should accept bids regardless of dsa when "required" any other value') + }); +}); diff --git a/test/spec/modules/dsp_genieeBidAdapter_spec.js b/test/spec/modules/dsp_genieeBidAdapter_spec.js new file mode 100644 index 00000000000..94ec1011fbf --- /dev/null +++ b/test/spec/modules/dsp_genieeBidAdapter_spec.js @@ -0,0 +1,173 @@ +import { expect } from 'chai'; +import { spec } from 'modules/dsp_genieeBidAdapter.js'; +import { config } from 'src/config'; + +describe('Geniee adapter tests', () => { + const validBidderRequest = { + code: 'sample_request', + bids: [{ + bidId: 'bid-id', + bidder: 'dsp_geniee', + params: { + test: 1 + } + }], + gdprConsent: { + gdprApplies: false + }, + uspConsent: '1YNY' + }; + + describe('isBidRequestValid function test', () => { + it('valid', () => { + expect(spec.isBidRequestValid(validBidderRequest.bids[0])).equal(true); + }); + }); + describe('buildRequests function test', () => { + it('auction', () => { + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + const auction_id = request.data.id; + expect(request).deep.equal({ + method: 'POST', + url: 'https://rt.gsspat.jp/prebid_auction', + data: { + at: 1, + id: auction_id, + imp: [ + { + ext: { + test: 1 + }, + id: 'bid-id' + } + ], + test: 1 + }, + }); + }); + it('uncomfortable (gdpr)', () => { + validBidderRequest.gdprConsent.gdprApplies = true; + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + validBidderRequest.gdprConsent.gdprApplies = false; + }); + it('uncomfortable (usp)', () => { + validBidderRequest.uspConsent = '1YYY'; + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + validBidderRequest.uspConsent = '1YNY'; + }); + it('uncomfortable (coppa)', () => { + config.setConfig({ coppa: true }); + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + config.resetConfig(); + }); + it('uncomfortable (currency)', () => { + config.setConfig({ currency: { adServerCurrency: 'TWD' } }); + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + config.resetConfig(); + }); + }); + describe('interpretResponse function test', () => { + it('sample bid', () => { + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + const auction_id = request.data.id; + const adm = "\n"; + const serverResponse = { + body: { + id: auction_id, + cur: 'JPY', + seatbid: [{ + bid: [{ + id: '7b77235d599e06d289e58ddfa9390443e22d7071', + impid: 'bid-id', + price: 0.6666000000000001, + adid: '8405715', + adm: adm, + adomain: ['geniee.co.jp'], + iurl: 'http://img.gsspat.jp/e/068c8e1eafbf0cb6ac1ee95c36152bd2/04f4bd4e6b71f978d343d84ecede3877.png', + cid: '8405715', + crid: '1383823', + cat: ['IAB1'], + w: 300, + h: 250, + mtype: 1 + }] + }] + } + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).deep.equal([{ + ad: adm, + cpm: 0.6666000000000001, + creativeId: '1383823', + creative_id: '1383823', + height: 250, + width: 300, + currency: 'JPY', + mediaType: 'banner', + meta: { + advertiserDomains: ['geniee.co.jp'] + }, + netRevenue: true, + requestId: 'bid-id', + seatBidId: '7b77235d599e06d289e58ddfa9390443e22d7071', + ttl: 300 + }]); + }); + it('no bid', () => { + const serverResponse = {}; + const bids = spec.interpretResponse(serverResponse, validBidderRequest); + expect(bids).deep.equal([]); + }); + }); + describe('getUserSyncs function test', () => { + it('sync enabled', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + const serverResponses = []; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).deep.equal([{ + type: 'image', + url: 'https://rt.gsspat.jp/prebid_cs' + }]); + }); + it('sync disabled (option false)', () => { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: false + }; + const serverResponses = []; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).deep.equal([]); + }); + it('sync disabled (gdpr)', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + const serverResponses = []; + const gdprConsent = { + gdprApplies: true + }; + const syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent); + expect(syncs).deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/dxkultureBidAdapter_spec.js b/test/spec/modules/dxkultureBidAdapter_spec.js new file mode 100644 index 00000000000..a752c81cb6e --- /dev/null +++ b/test/spec/modules/dxkultureBidAdapter_spec.js @@ -0,0 +1,649 @@ +import {expect} from 'chai'; +import {spec, SYNC_URL} from 'modules/dxkultureBidAdapter.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; + +const getBannerRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: 'bidderRequestId', + bids: [ + { + bidder: 'dxkulture', + params: { + placementId: 123456, + publisherId: 'publisherId', + bidfloor: 10, + }, + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + placementCode: 'div-gpt-dummy-placement-code', + mediaTypes: { + banner: { + sizes: [ + [ 300, 250 ], + ] + } + }, + bidId: '2e9f38ea93bb9e', + bidderRequestId: 'bidderRequestId', + } + ], + start: 1487883186070, + auctionStart: 1487883186069, + timeout: 3000 + } +}; + +const getVideoRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + bidderRequestId: '34feaad34lkj2', + bids: [{ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }, { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe2e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }], + auctionStart: 1520001292880, + timeout: 5000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'test.com' + } + }; +}; + +const getBidderResponse = () => { + return { + headers: null, + body: { + id: 'bid-response', + seatbid: [ + { + bid: [ + { + id: '2e9f38ea93bb9e', + impid: '2e9f38ea93bb9e', + price: 0.18, + adm: '', + adid: '144762342', + adomain: [ + 'https://dummydomain.com' + ], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + cat: [], + w: 300, + h: 250, + ext: { + prebid: { + type: 'banner' + }, + bidder: { + appnexus: { + brand_id: 334553, + auction_id: 514667951122925701, + bidder_id: 2, + bid_ad_type: 0 + } + } + } + } + ], + seat: 'dxkulture' + } + ], + ext: { + usersync: { + sovrn: { + status: 'none', + syncs: [ + { + url: 'urlsovrn', + type: 'iframe' + } + ] + }, + appnexus: { + status: 'none', + syncs: [ + { + url: 'urlappnexus', + type: 'pixel' + } + ] + } + }, + responsetimemillis: { + appnexus: 127 + } + } + } + }; +} + +describe('dxkultureBidAdapter', function() { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'dxkulture', + 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + 'bidderRequestId': '34feaad34lkj2', + 'bids': videoBidRequest, + 'auctionStart': 1520001292880, + 'timeout': 3000, + 'start': 1520001292884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 0 + } + }; + }); + + describe('isValidRequest', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('reject requests without params', function () { + bidderRequest.bids[0].params = {}; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + bidderRequest.bids[0].mediaTypes = {} + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + }); + + describe('buildRequests', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bidRequest.url).equal('https://ads.dxkulture.com/pbjs?pid=publisherId&placementId=123456'); + expect(bidRequest.method).equal('POST'); + }); + }); + + context('banner validation', function () { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'dxkulture', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'instream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(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, + '2:1', + 123, + 'test' + ]; + + 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, + 'test', + 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, + 'test', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + }); + + describe('buildRequests', function () { + let bidderBannerRequest; + let bidRequestsWithMediaTypes; + let mockBidderRequest; + + beforeEach(function() { + bidderBannerRequest = getBannerRequest(); + + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 + } + } + }, { + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + placement: 1, + plcmt: 1, + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); + + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest) + + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bidderBannerRequest.bids[0].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, getBannerRequest(), { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(bidderBannerRequest.bids, req); + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + + expect(data.imp[1].video.w).to.equal(width); + expect(data.imp[1].video.h).to.equal(height); + expect(data.imp[1].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.imp[1]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); + expect(data.imp[1]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should set pubId to e2etest when bid.params.e2etest = true', function () { + bidRequestsWithMediaTypes[0].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest'); + }); + + it('should attach End 2 End test data', function () { + bidRequestsWithMediaTypes[1].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + expect(data.imp[1].bidfloor).to.equal(0); + expect(data.imp[1].video.w).to.equal(640); + expect(data.imp[1].video.h).to.equal(480); + }); + }); + } + }); + + describe('interpretResponse', function() { + context('when mediaType is banner', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBannerRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('have bids', function () { + let bids = spec.interpretResponse(bidderResponse, bidRequest); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', getBidderResponse().body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', getBidderResponse().body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', getBidderResponse().body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', getBidderResponse().body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', getBidderResponse().body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', getBidderResponse().body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains'); + expect(bids[index]).to.have.property('ttl', 300); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + }); + + context('when mediaType is video', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + }); + }); + + describe('getUserSyncs', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should prioritize iframe', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/dynamicAdBoostRtdProvider_spec.js b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js new file mode 100644 index 00000000000..66c24435589 --- /dev/null +++ b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js @@ -0,0 +1,77 @@ +import { subModuleObj as rtdProvider } from 'modules/dynamicAdBoostRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { expect } from 'chai'; + +const configWithParams = { + params: { + keyId: 'dynamic', + adUnits: ['gpt-123'], + threshold: 1 + } +}; + +const configWithoutRequiredParams = { + params: { + keyId: '' + } +}; + +describe('dynamicAdBoost', function() { + let clock; + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(Date.now()); + }); + afterEach(function () { + sandbox.restore(); + }); + describe('init', function() { + describe('initialize without expected params', function() { + it('fails initalize when keyId is not present', function() { + expect(rtdProvider.init(configWithoutRequiredParams)).to.be.false; + }) + }) + + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(rtdProvider.init(configWithParams)).to.be.true; + clock.tick(1000); + expect(loadExternalScript.called).to.be.true; + }) + }); + }); +}) + +describe('markViewed tests', function() { + let sandbox; + const mockObserver = { + unobserve: sinon.spy() + }; + const makeElement = (id) => { + const el = document.createElement('div'); + el.setAttribute('id', id); + return el; + } + const mockEntry = { + target: makeElement('target_id') + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }) + + afterEach(function() { + sandbox.restore() + }) + + it('markViewed returns a function', function() { + expect(rtdProvider.markViewed(mockEntry, mockObserver)).to.be.a('function') + }); + + it('markViewed unobserves', function() { + const func = rtdProvider.markViewed(mockEntry, mockObserver); + func(); + expect(mockObserver.unobserve.calledOnce).to.be.true; + }); +}) diff --git a/test/spec/modules/edge226BidAdapter_spec.js b/test/spec/modules/edge226BidAdapter_spec.js new file mode 100644 index 00000000000..4819d8d4a4e --- /dev/null +++ b/test/spec/modules/edge226BidAdapter_spec.js @@ -0,0 +1,373 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/edge226BidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'edge226' + +describe('Edge226BidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, 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://ssp.dauup.com/pbjs'); + }); + + it('Returns general data 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', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + 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.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, 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 = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, 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([], bidderRequest); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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/eids_spec.js b/test/spec/modules/eids_spec.js index 9291ec88569..e1f2394ab27 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -29,6 +29,18 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('unifiedId: ext generation with provider', function() { + const userId = { + tdid: {'id': 'some-sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'adserver.org', + uids: [{id: 'some-sample_id', atype: 1, ext: { rtiPartner: 'TDID', provider: 'some.provider.com' }}] + }); + }); + describe('id5Id', function() { it('does not include an ext if not provided', function() { const userId = { @@ -238,6 +250,39 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('sovrn', function() { + const userId = { + sovrn: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('sovrn with ext', function() { + const userId = { + sovrn: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('magnite', function() { const userId = { magnite: {'id': 'sample_id'} @@ -271,6 +316,105 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('index', function() { + const userId = { + index: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('index with ext', function() { + const userId = { + index: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('openx', function () { + const userId = { + openx: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('openx with ext', function () { + const userId = { + openx: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('pubmatic', function() { + const userId = { + pubmatic: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('pubmatic with ext', function() { + const userId = { + pubmatic: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('liveIntentId; getValue call and NO ext', function() { const userId = { lipb: { @@ -547,6 +691,21 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('operaId', function() { + const userId = { + operaId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 't.adx.opera.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + it('33acrossId', function() { const userId = { '33acrossId': { diff --git a/test/spec/modules/enrichmentFpdModule_spec.js b/test/spec/modules/enrichmentFpdModule_spec.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 1a6cfd7afe4..a381d7644a1 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -74,6 +74,53 @@ describe('E-Planning Adapter', function () { }, 'sizes': [[300, 250], [300, 600]], }; + const validBidSpaceNameWithBidFloor = { + bidder: 'eplanning', + 'bidId': BID_ID, + params: { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'sizes': [[300, 250], [300, 600]], + }; + const validBidSpaceOutstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'playerSize': [300, 600], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; + const validBidSpaceInstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; const validBidSpaceOutstream = { 'bidder': 'eplanning', 'bidId': BID_ID, @@ -573,6 +620,26 @@ describe('E-Planning Adapter', function () { expect(e).to.equal(SN + ':300x250,300x600'); }); + it('should return e parameter with space name attribute with value according to the adunit sizes and bidFloor', function () { + const e = spec.buildRequests([validBidSpaceNameWithBidFloor], bidderRequest).data.e; + expect(e).to.equal(SN + ':300x250,300x600|' + validBidSpaceNameWithBidFloor.getFloor().floor); + }); + + it('should return correct e parameter with support vast with one space with size outstream and bidFloor', function () { + const data = spec.buildRequests([validBidSpaceOutstreamWithBidFloor], bidderRequest).data; + expect(data.e).to.equal('video_300x600_0:300x600;1|' + validBidSpaceOutstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(2); + expect(data.vv).to.equal(3); + }); + + it('should return correct e parameter with support vast with one space with size instream with bidFloor', function () { + let bidRequests = [validBidSpaceInstreamWithBidFloor]; + const data = spec.buildRequests(bidRequests, bidderRequest).data; + expect(data.e).to.equal('video_640x480_0:640x480;1|' + validBidSpaceInstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(1); + expect(data.vv).to.equal(3); + }); + it('should return correct e parameter with more than one adunit', function () { const NEW_CODE = ADUNIT_CODE + '2'; const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2'; diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 9a016b0facd..9ad2b69e89c 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -1,15 +1,13 @@ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { euidIdSubmodule } from 'modules/euidIdSystem.js'; +import {euidIdSubmodule} from 'modules/euidIdSystem.js'; import 'modules/consentManagement.js'; import 'src/prebid.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; -import { configureTimerInterceptors } from 'test/mocks/timers.js'; -import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; +import * as utils from 'src/utils.js'; +import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -24,30 +22,50 @@ const refreshedToken = 'refreshed-advertising-token'; const auctionDelayMs = 10; const makeEuidIdentityContainer = (token) => ({euid: {id: token}}); +const makeEuidOptoutContainer = (token) => ({euid: {optout: true}}); const useLocalStorage = true; + const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'euid', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}, ...extraSettings}] }, debug }); +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; +const optoutToken = 'optout-token'; + const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; +const cstgApiUrl = 'https://prod.euid.eu/v2/token/client-generate'; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); +const makeSuccessResponseBody = (token) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: token } })); +const makeOptoutResponseBody = (token) => btoa(JSON.stringify({ status: 'optout', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: token } })); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); +const expectOptout = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidOptoutContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { - let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let suiteSandbox, restoreSubtleToUndefined = false; + + const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureEuidCstgResponse = (httpStatus, response) => server.respondWith('POST', cstgApiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + before(function() { uninstallGdprEnforcement(); hook.ready(); suiteSandbox = sinon.sandbox.create(); if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function() { suiteSandbox.restore(); @@ -114,10 +132,33 @@ describe('EUID module', function() { it('When an expired token is provided and the API responds in time, the refreshed token is provided to the auction.', async function() { setGdprApplies(true); const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); - configureEuidResponse(200, makeSuccessResponseBody()); + configureEuidResponse(200, makeSuccessResponseBody(refreshedToken)); config.setConfig(makePrebidConfig({euidToken})); - apiHelpers.respondAfterDelay(1); + apiHelpers.respondAfterDelay(1, server); const bid = await runAuction(); expectToken(bid, refreshedToken); }); + + if (FEATURES.UID2_CSTG) { + it('Should use client side generated EUID token in the auction.', async function() { + setGdprApplies(true); + const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); + configureEuidCstgResponse(200, makeSuccessResponseBody(clientSideGeneratedToken)); + config.setConfig(makePrebidConfig({ euidToken, ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(1, server); + + const bid = await runAuction(); + expectToken(bid, clientSideGeneratedToken); + }); + it('Should receive an optout response when the user has opted out.', async function() { + setGdprApplies(true); + const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); + configureEuidCstgResponse(200, makeOptoutResponseBody(optoutToken)); + config.setConfig(makePrebidConfig({ euidToken, ...cstgConfigParams, email: 'optout@test.com' })); + apiHelpers.respondAfterDelay(1, server); + + const bid = await runAuction(); + expectOptout(bid, optoutToken); + }); + } }); diff --git a/test/spec/modules/experianRtdProvider_spec.js b/test/spec/modules/experianRtdProvider_spec.js new file mode 100644 index 00000000000..fd104674d70 --- /dev/null +++ b/test/spec/modules/experianRtdProvider_spec.js @@ -0,0 +1,365 @@ +import { + EXPERIAN_RTID_DATA_KEY, + EXPERIAN_RTID_EXPIRATION_KEY, + EXPERIAN_RTID_STALE_KEY, + SUBMODULE_NAME, + experianRtdObj, + experianRtdSubmodule, EXPERIAN_RTID_NO_TRACK_KEY +} from '../../../modules/experianRtdProvider.js'; +import { getStorageManager } from '../../../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../../../src/activities/modules'; +import { safeJSONParse, timestamp } from '../../../src/utils'; +import {server} from '../../mocks/xhr.js'; + +describe('Experian realtime module', () => { + const sandbox = sinon.createSandbox(); + let requests; + + const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }) + beforeEach(() => { + requests = server.requests; + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null) + }) + afterEach(() => { + sandbox.restore(); + }) + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'appnexus' } + ] + }] + }; + describe('init', () => { + it('succeeds when params have accountId', () => { + const initResult = experianRtdSubmodule.init({ params: { accountId: 'ZylatYg' } }) + expect(initResult).to.be.true; + }) + + it('fails when params don\'t have accountId', () => { + const initResult = experianRtdSubmodule.init({ }) + expect(initResult).to.be.false; + }) + }) + + describe('getBidRequestData', () => { + describe('when local storage has data, isn\'t no track, isn\'t stale and isn\'t expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t request data envelope, and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + + describe('when local storage has data but it is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('it requests data envelope and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + }) + }) + describe('when local storage has data but it is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no data envelope', () => { + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no track and is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and isn\'t expired or stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t alter bids and doesn\'t request data envelope', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + }) + + describe('alterBids', () => { + describe('data envelope has every bidder from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({pubmatic: { + experianRtidKey: 'pubmatic-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + describe('data envelope is missing bidders from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({ + sovrn: { + experianRtidKey: 'sovrn-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + }) + + describe('requestDataEnvelope', () => { + it('sends request to experian rtd and stores response', () => { + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.requestDataEnvelope(moduleConfig, { gdpr: { gdprApplies: 0, consentString: 'wow' }, uspConsent: '1YYY' }) + requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + '{"staleAt":"2023-06-01T00:00:00","expiresAt":"2023-06-03T00:00:00","status":"ok","data":[{"bidder":"pubmatic","data":{"key":"pubmatic-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}},{"bidder":"sovrn","data":{"key":"sovrn-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}}]}' + ) + + expect(requests[0].url).to.equal('https://rtid.tapad.com/acc/ZylatYg/ids?gdpr=0&gdpr_consent=wow&us_privacy=1YYY') + expect(safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null))).to.deep.equal([{bidder: 'pubmatic', data: {key: 'pubmatic-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}, {bidder: 'sovrn', data: {key: 'sovrn-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}]) + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY)).to.equal('2023-06-01T00:00:00') + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY)).to.equal('2023-06-03T00:00:00') + }) + }) + + describe('extractConsentQueryString', () => { + describe('when userConsent is empty', () => { + it('returns undefined', () => { + expect(experianRtdObj.extractConsentQueryString({})).to.be.undefined + }) + }) + + describe('when userConsent exists', () => { + it('builds query string', () => { + expect( + experianRtdObj.extractConsentQueryString({}, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' }) + ).to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY') + }) + }) + + describe('when config.ids exists', () => { + it('builds query string', () => { + expect(experianRtdObj.extractConsentQueryString({ params: { accountId: 'ZylatYg', ids: { maid: ['424', '2982'], hem: 'my-hem' } } }, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' })) + .to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY&id.maid=424&id.maid=2982&id.hem=my-hem') + }) + }) + }) +}) diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 152adba9d00..cb81c6f06de 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -621,7 +621,7 @@ describe('FeedAdAdapter', function () { expect(call.url).to.equal('https://api.feedad.com/1/prebid/web/events'); expect(JSON.parse(call.requestBody)).to.deep.equal(expectedData); expect(call.method).to.equal('POST'); - expect(call.requestHeaders).to.include({'Content-Type': 'application/json;charset=utf-8'}); + expect(call.requestHeaders).to.include({'Content-Type': 'application/json'}); }) }); }); diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js new file mode 100644 index 00000000000..8ab11171121 --- /dev/null +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -0,0 +1,177 @@ +import {onAuctionConfigFactory, setPAAPIConfigFactory, slotConfigurator} from 'modules/fledgeForGpt.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import {deepSetValue} from '../../../src/utils.js'; +import {config} from 'src/config.js'; + +describe('fledgeForGpt module', () => { + let sandbox, fledgeAuctionConfig; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('slotConfigurator', () => { + let mockGptSlot, setGptConfig; + beforeEach(() => { + mockGptSlot = { + setConfig: sinon.stub(), + getAdUnitPath: () => 'mock/gpt/au' + }; + sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); + setGptConfig = slotConfigurator(); + }); + it('should set GPT slot config', () => { + setGptConfig('au', [fledgeAuctionConfig]); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au'); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: fledgeAuctionConfig, + }] + }); + }); + + describe('when reset = true', () => { + it('should reset GPT slot config', () => { + setGptConfig('au', [fledgeAuctionConfig]); + mockGptSlot.setConfig.resetHistory(); + gptUtils.getGptSlotForAdUnitCode.resetHistory(); + setGptConfig('au', [], true); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au'); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: null + }] + }); + }); + + it('should reset only sellers with no fresh config', () => { + setGptConfig('au', [{seller: 's1'}, {seller: 's2'}]); + mockGptSlot.setConfig.resetHistory(); + setGptConfig('au', [{seller: 's1'}], true); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 's1', + auctionConfig: {seller: 's1'} + }, { + configKey: 's2', + auctionConfig: null + }] + }) + }); + + it('should not reset sellers that were already reset', () => { + setGptConfig('au', [{seller: 's1'}]); + setGptConfig('au', [], true); + mockGptSlot.setConfig.resetHistory(); + setGptConfig('au', [], true); + sinon.assert.notCalled(mockGptSlot.setConfig); + }) + + it('should keep track of configuration history by slot', () => { + setGptConfig('au1', [{seller: 's1'}]); + setGptConfig('au1', [{seller: 's2'}], false); + setGptConfig('au2', [{seller: 's3'}]); + mockGptSlot.setConfig.resetHistory(); + setGptConfig('au1', [], true); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 's1', + auctionConfig: null + }, { + configKey: 's2', + auctionConfig: null + }] + }); + }) + }); + }); + describe('onAuctionConfig', () => { + [ + 'fledgeForGpt', + 'paapi.gpt' + ].forEach(namespace => { + describe(`using ${namespace} config`, () => { + Object.entries({ + 'omitted': [undefined, true], + 'enabled': [true, true], + 'disabled': [false, false] + }).forEach(([t, [autoconfig, shouldSetConfig]]) => { + describe(`when autoconfig is ${t}`, () => { + beforeEach(() => { + const cfg = {}; + deepSetValue(cfg, `${namespace}.autoconfig`, autoconfig); + config.setConfig(cfg); + }); + afterEach(() => { + config.resetConfig(); + }); + + it(`should ${shouldSetConfig ? '' : 'NOT'} set GPT slot configuration`, () => { + const auctionConfig = {componentAuctions: [{seller: 'mock1'}, {seller: 'mock2'}]}; + const setGptConfig = sinon.stub(); + const markAsUsed = sinon.stub(); + onAuctionConfigFactory(setGptConfig)('aid', {au1: auctionConfig, au2: null}, markAsUsed); + if (shouldSetConfig) { + sinon.assert.calledWith(setGptConfig, 'au1', auctionConfig.componentAuctions); + sinon.assert.calledWith(setGptConfig, 'au2', []); + sinon.assert.calledWith(markAsUsed, 'au1'); + } else { + sinon.assert.notCalled(setGptConfig); + sinon.assert.notCalled(markAsUsed); + } + }); + }) + }) + }) + }) + }); + describe('setPAAPIConfigForGpt', () => { + let getPAAPIConfig, setGptConfig, setPAAPIConfigForGPT; + beforeEach(() => { + getPAAPIConfig = sinon.stub(); + setGptConfig = sinon.stub(); + setPAAPIConfigForGPT = setPAAPIConfigFactory(getPAAPIConfig, setGptConfig); + }); + + Object.entries({ + missing: null, + empty: {} + }).forEach(([t, configs]) => { + it(`does not set GPT slot config when config is ${t}`, () => { + getPAAPIConfig.returns(configs); + setPAAPIConfigForGPT('mock-filters'); + sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); + sinon.assert.notCalled(setGptConfig); + }) + }); + + it('sets GPT slot config for each ad unit that has PAAPI config, and resets the rest', () => { + const cfg = { + au1: { + componentAuctions: [{seller: 's1'}, {seller: 's2'}] + }, + au2: { + componentAuctions: [{seller: 's3'}] + }, + au3: null + } + getPAAPIConfig.returns(cfg); + setPAAPIConfigForGPT('mock-filters'); + sinon.assert.calledWith(getPAAPIConfig, 'mock-filters'); + Object.entries(cfg).forEach(([au, config]) => { + sinon.assert.calledWith(setGptConfig, au, config?.componentAuctions ?? [], true); + }) + }); + }) +}); diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js deleted file mode 100644 index a81ff05596e..00000000000 --- a/test/spec/modules/fledge_spec.js +++ /dev/null @@ -1,208 +0,0 @@ -import { - expect -} from 'chai'; -import * as fledge from 'modules/fledgeForGpt.js'; -import {config} from '../../../src/config.js'; -import adapterManager from '../../../src/adapterManager.js'; -import * as utils from '../../../src/utils.js'; -import {hook} from '../../../src/hook.js'; -import 'modules/appnexusBidAdapter.js'; -import 'modules/rubiconBidAdapter.js'; -import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; - -const CODE = 'sampleBidder'; -const AD_UNIT_CODE = 'mock/placement'; - -describe('fledgeForGpt module', function() { - let nextFnSpy; - fledge.init({enabled: true}) - - const bidRequest = { - adUnitCode: AD_UNIT_CODE, - bids: [{ - bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: AD_UNIT_CODE, - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - } - - describe('addComponentAuctionHook', function() { - beforeEach(function() { - nextFnSpy = sinon.spy(); - }); - - it('should call next() when a proper adUnitCode and fledgeAuctionConfig are provided', function() { - fledge.addComponentAuctionHook(nextFnSpy, bidRequest.adUnitCode, fledgeAuctionConfig); - expect(nextFnSpy.called).to.be.true; - }); - }); -}); - -describe('fledgeEnabled', function () { - const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])) - - before(function () { - // navigator.runAdAuction & co may not exist, so we can't stub it normally with - // sinon.stub(navigator, 'runAdAuction') or something - Object.keys(navProps).forEach(p => { navigator[p] = sinon.stub() }); - hook.ready(); - }); - - after(function() { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); - }) - - afterEach(function () { - config.resetConfig(); - }); - - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - } - }); - - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - }); -}); - -describe('ortb processors for fledge', () => { - describe('imp.ext.ae', () => { - it('should be removed if fledge is not enabled', () => { - const imp = {ext: {ae: 1}}; - setImpExtAe(imp, {}, {bidderRequest: {}}); - expect(imp.ext.ae).to.not.exist; - }) - it('should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: false}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); - expect(imp.ext.ae).to.equal(false); - }); - }); - describe('parseExtPrebidFledge', () => { - function packageConfigs(configs) { - return { - ext: { - prebid: { - fledge: { - auctionconfigs: configs - } - } - } - } - } - - function generateImpCtx(fledgeFlags) { - return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); - } - - function generateCfg(impid, ...ids) { - return ids.map((id) => ({impid, config: {id}})); - } - - function extractResult(ctx) { - return Object.fromEntries( - Object.entries(ctx) - .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) - .filter(([_, val]) => val != null) - ); - } - - it('should collect fledge configs by imp', () => { - const ctx = { - impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) - }; - const resp = packageConfigs( - generateCfg('e1', 1, 2, 3) - .concat(generateCfg('e2', 4) - .concat(generateCfg('d1', 5, 6))) - ); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({ - e1: [1, 2, 3], - e2: [4], - }); - }); - it('should not choke if fledge config references unknown imp', () => { - const ctx = {impContext: generateImpCtx({i: 1})}; - const resp = packageConfigs(generateCfg('unknown', 1)); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({}); - }); - }); - describe('setResponseFledgeConfigs', () => { - it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { - const ctx = { - impContext: { - 1: { - bidRequest: {bidId: 'bid1'}, - fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] - }, - 2: { - bidRequest: {bidId: 'bid2'}, - fledgeConfigs: [{config: {id: 3}}] - }, - 3: { - bidRequest: {bidId: 'bid3'} - } - } - }; - const resp = {}; - setResponseFledgeConfigs(resp, {}, ctx); - expect(resp.fledgeAuctionConfigs).to.eql([ - {bidId: 'bid1', config: {id: 1}}, - {bidId: 'bid1', config: {id: 2}}, - {bidId: 'bid2', config: {id: 3}}, - ]); - }); - it('should not set fledgeAuctionConfigs if none exist', () => { - const resp = {}; - setResponseFledgeConfigs(resp, {}, { - impContext: { - 1: { - fledgeConfigs: [] - }, - 2: {} - } - }); - expect(resp).to.eql({}); - }); - }); -}); diff --git a/test/spec/modules/flippBidAdapter_spec.js b/test/spec/modules/flippBidAdapter_spec.js new file mode 100644 index 00000000000..518052ad91e --- /dev/null +++ b/test/spec/modules/flippBidAdapter_spec.js @@ -0,0 +1,170 @@ +import {expect} from 'chai'; +import {spec} from 'modules/flippBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +describe('flippAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'flipp', + params: { + publisherNameIdentifier: 'random', + siteId: 1234, + zoneIds: [1, 2, 3, 4], + } + }; + 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 invalidBid = Object.assign({}, bid); + invalidBid.params = { siteId: 1234 } + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + bidder: 'flipp', + params: { + siteId: 1234, + }, + adUnitCode: '/10000/unit_code', + sizes: [[300, 600]], + mediaTypes: {banner: {sizes: [[300, 600]]}}, + bidId: '237f4d1a293f99', + bidderRequestId: '1a857fa34c1c96', + auctionId: 'a297d1aa-7900-4ce4-a0aa-caa8d46c4af7', + transactionId: '00b2896c-2731-4f01-83e4-7a3ad5da13b6', + }]; + const bidderRequest = { + refererInfo: { + referer: 'http://example.com' + } + }; + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to ENDPOINT with query parameter', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function() { + it('should get correct bid response', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [{ + 'bidCpm': 1, + 'adId': 262838368, + 'height': 600, + 'width': 300, + 'storefront': { 'flyer_id': 5435567 }, + 'prebid': { + 'requestId': '237f4d1a293f99', + 'cpm': 1.11, + 'creative': 'Returned from server', + } + }] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const expectedResponse = [ + { + bidderCode: 'flipp', + requestId: '237f4d1a293f99', + currency: 'USD', + cpm: 1.11, + netRevenue: true, + width: 300, + height: 600, + creativeId: 262838368, + ttl: 30, + ad: 'Returned from server', + } + ]; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectedResponse); + }); + + it('should get empty bid response when no ad is returned', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(0); + expect(result).to.deep.have.same.members([]); + }) + + it('should get empty response when bid server returns 204', function() { + expect(spec.interpretResponse({})).to.be.empty; + }); + }); +}); diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index d970f70ad85..ff6f8562a4e 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -99,6 +99,79 @@ describe('fluctAdapter', function () { expect(request.data.page).to.eql('http://example.com'); }); + it('sends no transactionId by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.transactionId).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.tid as transactionId', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + tid: 'tid', + } + }, + })), bidderRequest)[0]; + expect(request.data.transactionId).to.eql('tid'); + }); + + it('sends no gpid by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.gpid).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.gpid as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + gpid: 'gpid', + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('gpid'); + }); + + it('sends ortb2Imp.ext.data.pbadslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-pbadslot'); + }); + + it('sends ortb2Imp.ext.data.adserver.adslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-adserver-adslot'); + }); + it('includes data.user.eids = [] by default', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.data.user.eids).to.eql([]); @@ -119,14 +192,14 @@ describe('fluctAdapter', function () { expect(request.data.regs).to.eql(undefined); }); - it('includes filtered user.eids if any exist', function () { + it('includes filtered user.eids if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { userIdAsEids: [ { source: 'foobar.com', uids: [ - { id: 'foobar-id' } + { id: 'foobar-id' }, ], }, { @@ -138,19 +211,19 @@ describe('fluctAdapter', function () { { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ], @@ -158,36 +231,96 @@ describe('fluctAdapter', function () { ); const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; expect(request.data.user.eids).to.eql([ + { + source: 'foobar.com', + uids: [ + { id: 'foobar-id' }, + ], + }, { source: 'adserver.org', uids: [ - { id: 'tdid' } + { id: 'tdid' }, ], }, { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ]); }); + it('includes user.data if any exists', function () { + const bidderRequest2 = Object.assign({}, bidderRequest, { + ortb2: { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }, + }, + }, + }); + const request = spec.buildRequests(bidRequests, bidderRequest2)[0]; + expect(request.data.user).to.eql({ + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }); + }); + it('includes data.params.kv if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { params: { kv: { imsids: ['imsid1', 'imsid2'] @@ -204,7 +337,7 @@ describe('fluctAdapter', function () { it('includes data.schain if any exists', function () { // this should be done by schain.js const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { schain: { ver: '1.0', complete: 1, @@ -271,7 +404,7 @@ describe('fluctAdapter', function () { }); }); - describe('interpretResponse', function() { + describe('should interpretResponse', function() { const callBeaconSnippet = '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 334553, - 'auction_id': 514667951122925701, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - }, - { - 'id': 'bidId2', - 'impid': 'bidId2', - 'price': 0.1, - 'adm': '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 386046, - 'auction_id': 517067951122925501, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'kulturemedia' - } - ], - 'ext': { - 'usersync': { - 'sovrn': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlsovrn', - 'type': 'iframe' - } - ] - }, - 'appnexus': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlappnexus', - 'type': 'pixel' - } - ] - } - }, - 'responsetimemillis': { - 'appnexus': 127 - } - } - } -}; - -const DEFAULT_NETWORK_ID = 1; - -describe('kulturemediaBidAdapter:', function () { - let videoBidRequest; - - const VIDEO_REQUEST = { - 'bidderCode': 'kulturemedia', - 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', - 'bidderRequestId': '34feaad34lkj2', - 'bids': videoBidRequest, - 'auctionStart': 1520001292880, - 'timeout': 3000, - 'start': 1520001292884, - 'doneCbCallCount': 0, - 'refererInfo': { - 'numIframes': 1, - 'reachedTop': true, - 'referer': 'test.com' - } - }; - - beforeEach(function () { - videoBidRequest = { - mediaTypes: { - video: { - context: 'instream', - playerSize: [[640, 480]], - } - }, - bidder: 'kulturemedia', - sizes: [640, 480], - bidId: '30b3efwfwe1e', - adUnitCode: 'video1', - params: { - video: { - playerWidth: 640, - playerHeight: 480, - mimes: ['video/mp4', 'application/javascript'], - protocols: [2, 5], - api: [2], - position: 1, - delivery: [2], - sid: 134, - rewarded: 1, - placement: 1, - hp: 1, - inventoryid: 123 - }, - site: { - id: 1, - page: 'https://test.com', - referrer: 'http://test.com' - }, - publisherId: 'km123' - } - }; - }); - - describe('isBidRequestValid', function () { - context('basic validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should accept request if placementId and publisherId are passed', function () { - expect(spec.isBidRequestValid(this.bid)).to.be.true; - }); - - it('reject requests without params', function () { - this.bid.params = {}; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when banner mediaType does not exist', function () { - this.bid.mediaTypes = {} - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - context('banner validation', function () { - it('returns true when banner sizes are defined', function () { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.true; - }); - - it('returns false when banner sizes are invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((sizes) => { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); - }); - - context('video validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - video: { - playerSize: [[300, 50]], - context: 'instream', - mimes: ['foo', 'bar'], - protocols: [1, 2] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should return true (skip validations) when e2etest = true', function () { - this.bid.params = { - e2etest: true - }; - expect(spec.isBidRequestValid(this.bid)).to.equal(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, - '2:1', - 123, - 'test' - ]; - - 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, - 'test', - 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, - 'test', - 1, - [] - ] - - invalidMimes.forEach((protocols) => { - this.bid.mediaTypes.video.protocols = protocols; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - }); - }); - - describe('buildRequests', function () { - context('when mediaType is banner', function () { - it('creates request data', function () { - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); - - expect(request).to.exist.and.to.be.a('object'); - const payload = JSON.parse(request.data); - expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId); - expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId); - }); - - it('has gdpr data if applicable', function () { - const req = Object.assign({}, BANNER_REQUEST, { - gdprConsent: { - consentString: 'consentString', - gdprApplies: true, - } - }); - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); - expect(payload.regs.ext).to.have.property('gdpr', 1); - }); - - it('should properly forward eids parameters', function () { - const req = Object.assign({}, BANNER_REQUEST); - req.bidRequest[0].userIdAsEids = [ - { - source: 'dummy.com', - uids: [ - { - id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', - atype: 1 - } - ] - }]; - let request = spec.buildRequests(req.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); - expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); - expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); - }); - }); - - context('when mediaType is video', function () { - it('should create a POST request for every bid', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach request data', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - const [width, height] = videoBidRequest.sizes; - const VERSION = '1.0.0'; - expect(data.imp[0].video.w).to.equal(width); - expect(data.imp[0].video.h).to.equal(height); - expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor); - expect(data.ext.prebidver).to.equal('$prebid.version$'); - expect(data.ext.adapterver).to.equal(spec.VERSION); - }); - - it('should set pubId to e2etest when bid.params.e2etest = true', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach End 2 End test data', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - expect(data.imp[0].bidfloor).to.not.exist; - expect(data.imp[0].video.w).to.equal(640); - expect(data.imp[0].video.h).to.equal(480); - }); - }); - }); - - describe('interpretResponse', function () { - context('when mediaType is banner', function () { - it('have bids', function () { - let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); - expect(bids).to.be.an('array').that.is.not.empty; - validateBidOnIndex(0); - validateBidOnIndex(1); - - function validateBidOnIndex(index) { - expect(bids[index]).to.have.property('currency', 'USD'); - expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); - expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); - expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); - expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); - expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); - expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); - expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain); - expect(bids[index]).to.have.property('ttl', 300); - expect(bids[index]).to.have.property('netRevenue', true); - } - }); - - it('handles empty response', function () { - const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); - const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); - - expect(bids).to.be.empty; - }); - }); - - context('when mediaType is video', function () { - it('should return no bids if the response is not valid', function () { - const bidResponse = spec.interpretResponse({ - body: null - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "nurl" and "adm" are missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - price: 6.01 - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "price" is missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - adm: '' - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return a valid video bid response with just "adm"', function () { - const serverResponse = { - id: '123', - seatbid: [{ - bid: [{ - id: 1, - adid: 123, - impid: 456, - crid: 2, - price: 6.01, - adm: '', - adomain: [ - 'kulturemedia.com' - ], - w: 640, - h: 480, - ext: { - prebid: { - type: 'video' - }, - } - }] - }], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - let o = { - requestId: serverResponse.seatbid[0].bid[0].impid, - ad: '', - cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: serverResponse.seatbid[0].bid[0].crid, - vastXml: serverResponse.seatbid[0].bid[0].adm, - width: 640, - height: 480, - mediaType: 'video', - currency: 'USD', - ttl: 300, - netRevenue: true, - meta: { - advertiserDomains: ['kulturemedia.com'] - } - }; - expect(bidResponse[0]).to.deep.equal(o); - }); - - 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}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl above 3601, default to 300', function () { - videoBidRequest.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}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl below 1, default to 300', function () { - videoBidRequest.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}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - }); - }); - - describe('getUserSyncs', function () { - it('handles no parameters', function () { - let opts = spec.getUserSyncs({}); - expect(opts).to.be.an('array').that.is.empty; - }); - it('returns non if sync is not allowed', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); - - expect(opts).to.be.an('array').that.is.empty; - }); - - it('iframe sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('iframe'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); - }); - - it('pixel sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); - }); - - it('all sync enabled should return all results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(2); - }); - }); -}) -; diff --git a/test/spec/modules/lassoBidAdapter_spec.js b/test/spec/modules/lassoBidAdapter_spec.js index 3695889aca0..ad4040c0452 100644 --- a/test/spec/modules/lassoBidAdapter_spec.js +++ b/test/spec/modules/lassoBidAdapter_spec.js @@ -126,6 +126,7 @@ describe('lassoBidAdapter', function () { it('should get the correct bid response', function () { let expectedResponse = { requestId: '123456789', + bidId: '123456789', cpm: 1, currency: 'USD', width: 728, diff --git a/test/spec/modules/limelightDigitalBidAdapter_spec.js b/test/spec/modules/limelightDigitalBidAdapter_spec.js index 0e6f4817e5e..6348d6a1ac6 100644 --- a/test/spec/modules/limelightDigitalBidAdapter_spec.js +++ b/test/spec/modules/limelightDigitalBidAdapter_spec.js @@ -17,6 +17,9 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, + refererInfo: { + page: 'https://publisher.com/page1' + }, placementCode: 'placement_0', auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', mediaTypes: { @@ -65,6 +68,9 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, + refererInfo: { + page: 'https://publisher.com/page2' + }, placementCode: 'placement_1', auctionId: '482f88de-29ab-45c8-981a-d25e39454a34', sizes: [[350, 200]], @@ -115,6 +121,9 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, + refererInfo: { + page: 'https://publisher.com/page3' + }, placementCode: 'placement_2', auctionId: 'e4771143-6aa7-41ec-8824-ced4342c96c8', sizes: [[800, 600]], @@ -162,6 +171,9 @@ describe('limelightDigitalAdapter', function () { custom4: 'custom4', custom5: 'custom5' }, + refererInfo: { + page: 'https://publisher.com/page4' + }, placementCode: 'placement_2', auctionId: 'e4771143-6aa7-41ec-8824-ced4342c96c8', video: { @@ -237,7 +249,8 @@ describe('limelightDigitalAdapter', function () { 'custom2', 'custom3', 'custom4', - 'custom5' + 'custom5', + 'page' ); expect(adUnit.id).to.be.a('number'); expect(adUnit.bidId).to.be.a('string'); @@ -251,6 +264,7 @@ describe('limelightDigitalAdapter', function () { expect(adUnit.custom3).to.be.a('string'); expect(adUnit.custom4).to.be.a('string'); expect(adUnit.custom5).to.be.a('string'); + expect(adUnit.page).to.be.a('string'); }) }) }) @@ -685,4 +699,5 @@ function validateAdUnit(adUnit, bid) { expect(adUnit.publisherId).to.equal(bid.params.publisherId); expect(adUnit.userIdAsEids).to.deep.equal(bid.userIdAsEids); expect(adUnit.supplyChain).to.deep.equal(bid.schain); + expect(adUnit.page).to.equal(bid.refererInfo.page); } diff --git a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js index fa4c5cd8cad..d00bfbc7bb5 100644 --- a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js +++ b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js @@ -16,7 +16,7 @@ let events = require('src/events'); let constants = require('src/constants.json'); let auctionId = '99abbc81-c1f1-41cd-8f25-f7149244c897' -const config = { +const configWithSamplingAll = { provider: 'liveintent', options: { bidWonTimeout: 2000, @@ -24,6 +24,14 @@ const config = { } } +const configWithSamplingNone = { + provider: 'liveintent', + options: { + bidWonTimeout: 2000, + sampling: 0 + } +} + let args = { auctionId: auctionId, timestamp: 1660915379703, @@ -273,8 +281,8 @@ describe('LiveIntent Analytics Adapter ', () => { clock.restore(); }); - it('request is computed and sent correctly', () => { - liAnalytics.enableAnalytics(config); + it('request is computed and sent correctly when sampling is 1', () => { + liAnalytics.enableAnalytics(configWithSamplingAll); sandbox.stub(utils, 'generateUUID').returns(instanceId); sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); @@ -288,7 +296,23 @@ describe('LiveIntent Analytics Adapter ', () => { it('track is called', () => { sandbox.stub(liAnalytics, 'track'); - liAnalytics.enableAnalytics(config); + liAnalytics.enableAnalytics(configWithSamplingAll); expectEvents().to.beTrackedBy(liAnalytics.track); }) + + it('no request is computed when sampling is 0', () => { + liAnalytics.enableAnalytics(configWithSamplingNone); + sandbox.stub(utils, 'generateUUID').returns(instanceId); + sandbox.stub(refererDetection, 'getRefererInfo').returns({page: url}); + sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); + events.emit(constants.EVENTS.AUCTION_END, args); + clock.tick(2000); + expect(server.requests.length).to.equal(0); + }); + + it('track is not called', () => { + sandbox.stub(liAnalytics, 'track'); + liAnalytics.enableAnalytics(configWithSamplingNone); + sinon.assert.callCount(liAnalytics.track, 0); + }) }); diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js index 92a15241e3a..e280d9108a0 100644 --- a/test/spec/modules/liveIntentIdMinimalSystem_spec.js +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -2,6 +2,7 @@ import * as utils from 'src/utils.js'; import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; +import * as refererDetection from '../../../src/refererDetection.js'; const PUBLISHER_ID = '89899'; const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; @@ -14,6 +15,7 @@ describe('LiveIntentMinimalId', function() { let getCookieStub; let getDataFromLocalStorageStub; let imgStub; + let refererInfoStub; beforeEach(function() { liveIntentIdSubmodule.setModuleMode('minimal'); @@ -23,6 +25,7 @@ describe('LiveIntentMinimalId', function() { logErrorStub = sinon.stub(utils, 'logError'); uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); }); afterEach(function() { @@ -32,6 +35,7 @@ describe('LiveIntentMinimalId', function() { logErrorStub.restore(); uspConsentDataStub.restore(); gdprConsentDataStub.restore(); + refererInfoStub.restore(); liveIntentIdSubmodule.setModuleMode('minimal'); resetLiveIntentIdSubmodule(); }); @@ -73,7 +77,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the Identity Exchange endpoint with the privided distributorId', function() { + it('should call the Identity Exchange endpoint with the provided distributorId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; @@ -87,7 +91,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint without the privided distributorId when appId is provided', function() { + it('should call the Identity Exchange endpoint without the provided distributorId when appId is provided', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; @@ -241,7 +245,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should decode a uid2 to a seperate object when present', function() { + it('should decode a uid2 to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); @@ -251,21 +255,48 @@ describe('LiveIntentMinimalId', function() { expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a bidswitch id to a seperate object when present', function() { + it('should decode a bidswitch id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a medianet id to a seperate object when present', function() { + it('should decode a medianet id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a magnite id to a seperate object when present', function() { + it('should decode a sovrn id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); + it('should decode an index id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a separate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a thetradedesk id to a separate object when present', function() { + const provider = 'liveintent.com' + refererInfoStub.returns({domain: provider}) + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'tdid': 'bar'}, 'tdid': {'id': 'bar', 'ext': {'rtiPartner': 'TDID', 'provider': provider}}}); + }); + it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 6b7dfb82d48..c6108b49715 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,7 +1,9 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; +import * as refererDetection from '../../../src/refererDetection.js'; + resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') const PUBLISHER_ID = '89899'; @@ -12,9 +14,11 @@ describe('LiveIntentId', function() { let logErrorStub; let uspConsentDataStub; let gdprConsentDataStub; + let gppConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; + let refererInfoStub; beforeEach(function() { liveIntentIdSubmodule.setModuleMode('standard'); @@ -24,6 +28,8 @@ describe('LiveIntentId', function() { logErrorStub = sinon.stub(utils, 'logError'); uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); }); afterEach(function() { @@ -33,6 +39,8 @@ describe('LiveIntentId', function() { logErrorStub.restore(); uspConsentDataStub.restore(); gdprConsentDataStub.restore(); + gppConsentDataStub.restore(); + refererInfoStub.restore(); resetLiveIntentIdSubmodule(); }); @@ -42,11 +50,15 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1, 2] + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*/); + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1%2C2.*/); const response = { unifiedId: 'a_unified_id', segments: [123, 234] @@ -65,9 +77,13 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.getId(defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -83,6 +99,16 @@ describe('LiveIntentId', function() { }, 200); }); + it('should initialize LiveConnect and forward the prebid version when decode and emit an event', function(done) { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams + }}); + setTimeout(() => { + expect(server.requests[0].url).to.contain('tv=$prebid.version$') + done(); + }, 200); + }); + it('should initialize LiveConnect with the config params when decode and emit an event', function (done) { liveIntentIdSubmodule.decode({}, { params: { ...defaultConfigParams.params, @@ -123,9 +149,13 @@ describe('LiveIntentId', function() { gdprApplies: false, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -171,7 +201,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -179,13 +209,13 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint with the privided distributorId', function() { + it('should call the Identity Exchange endpoint with the provided distributorId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -193,13 +223,13 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint without the privided distributorId when appId is provided', function() { + it('should call the Identity Exchange endpoint without the provided distributorId when appId is provided', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -219,7 +249,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?cd=.localhost&resolve=nonId'); request.respond( 200, responseHeader, @@ -234,7 +264,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 200, responseHeader, @@ -249,7 +279,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 503, responseHeader, @@ -266,7 +296,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&resolve=nonId`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&resolve=nonId`); request.respond( 200, responseHeader, @@ -289,7 +319,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc&resolve=nonId`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&_thirdPC=third-pc&resolve=nonId`); request.respond( 200, responseHeader, @@ -311,7 +341,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); request.respond( 200, responseHeader, @@ -344,7 +374,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=nonId&resolve=foo`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId&resolve=foo`); request.respond( 200, responseHeader, @@ -353,7 +383,7 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should decode a uid2 to a seperate object when present', function() { + it('should decode a uid2 to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); @@ -363,21 +393,48 @@ describe('LiveIntentId', function() { expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a bidswitch id to a seperate object when present', function() { + it('should decode a bidswitch id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a medianet id to a seperate object when present', function() { + it('should decode a medianet id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a magnite id to a seperate object when present', function() { + it('should decode a sovrn id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); + it('should decode an index id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a separate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a thetradedesk id to a separate object when present', function() { + const provider = 'liveintent.com' + refererInfoStub.returns({domain: provider}) + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', thetradedesk: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'tdid': 'bar'}, 'tdid': {'id': 'bar', 'ext': {'rtiPartner': 'TDID', 'provider': provider}}}); + }); + it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { @@ -386,7 +443,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=uid2`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=uid2`); request.respond( 200, responseHeader, diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index 52eaf8d7d76..5ab00859d81 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -38,10 +38,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', ortb2Imp: { ext: { - tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', } }, - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' } ], start: 1472239426002, @@ -120,8 +119,49 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', - formats: [{width: 980, height: 240}, {width: 980, height: 120}] + formats: [{width: 980, height: 240}, {width: 980, height: 120}], + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + } + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should send ortb2Imp', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + let ortb2ImpRequest = clone(bidderRequest); + ortb2ImpRequest.bids[0].ortb2Imp.ext.data = {key: 'value'}; + let result = spec.buildRequests(ortb2ImpRequest.bids, ortb2ImpRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + formats: [{width: 980, height: 240}, {width: 980, height: 120}], + rtbData: { + ext: { + data: {key: 'value'}, + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + } }] }; @@ -157,12 +197,20 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }, { callerAdUnitId: 'box_d_1', bidId: '3ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 300, height: 250}] }] }; @@ -194,7 +242,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'caller id 1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -225,7 +277,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -256,7 +312,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -289,7 +349,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -322,7 +386,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -352,7 +420,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}], options: {keyvalues: [{key: 'key', value: 'value'}]} }] @@ -384,7 +456,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -414,7 +490,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}], native: {'nativedata': 'content parsed serverside only'} }] @@ -445,7 +525,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}], native: {'nativedata': 'content parsed serverside only'}, banner: true @@ -477,7 +561,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}], video: {'videodata': 'content parsed serverside only'} }] @@ -525,7 +613,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -555,7 +647,11 @@ describe('Livewrapped adapter tests', function () { adRequests: [{ callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 728, height: 90}] }] }; @@ -592,7 +688,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -627,7 +727,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -660,7 +764,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -700,7 +808,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -730,7 +842,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -760,7 +876,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -810,7 +930,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -842,7 +966,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -876,7 +1004,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -910,7 +1042,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -946,7 +1082,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -982,7 +1122,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -1018,7 +1162,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}] }] }; @@ -1063,7 +1211,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}], flr: 10 }] @@ -1101,7 +1253,11 @@ describe('Livewrapped adapter tests', function () { adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', callerAdUnitId: 'panorama_d_1', bidId: '2ffb201a808da7', - transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + rtbData: { + ext: { + tid: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D' + }, + }, formats: [{width: 980, height: 240}, {width: 980, height: 120}], flr: 10 }] diff --git a/test/spec/modules/lm_kiviadsBidAdapter_spec.js b/test/spec/modules/lm_kiviadsBidAdapter_spec.js new file mode 100644 index 00000000000..68ac73289cd --- /dev/null +++ b/test/spec/modules/lm_kiviadsBidAdapter_spec.js @@ -0,0 +1,455 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec, getBidFloor} from 'modules/lm_kiviadsBidAdapter.js'; +import {deepClone} from 'src/utils'; + +const ENDPOINT = 'https://pbjs.kiviads.live'; + +const defaultRequest = { + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; +describe('lm_kiviadsBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'lm_kiviads', + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['lm_kiviads'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['lm_kiviads']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequestVideo}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}) diff --git a/test/spec/modules/logicadBidAdapter_spec.js b/test/spec/modules/logicadBidAdapter_spec.js index 3c1383781b9..12e8ca31cbb 100644 --- a/test/spec/modules/logicadBidAdapter_spec.js +++ b/test/spec/modules/logicadBidAdapter_spec.js @@ -36,6 +36,11 @@ describe('LogicadAdapter', function () { } }] }], + ortb2Imp: { + ext: { + ae: 1 + } + }, ortb2: { device: { sua: { @@ -176,7 +181,8 @@ describe('LogicadAdapter', function () { numIframes: 1, stack: [] }, - auctionStart: 1563337198010 + auctionStart: 1563337198010, + fledgeEnabled: true }; const serverResponse = { body: { @@ -203,6 +209,49 @@ describe('LogicadAdapter', function () { } } }; + + const paapiServerResponse = { + body: { + seatbid: + [{ + bid: { + requestId: '51ef8751f9aead', + cpm: 101.0234, + width: 300, + height: 250, + creativeId: '2019', + currency: 'JPY', + netRevenue: true, + ttl: 60, + ad: '
TEST
', + meta: { + advertiserDomains: ['logicad.com'] + } + } + }], + ext: { + fledgeAuctionConfigs: [{ + bidId: '51ef8751f9aead', + config: { + seller: 'https://fledge.ladsp.com', + decisionLogicUrl: 'https://fledge.ladsp.com/decision_logic.js', + interestGroupBuyers: ['https://fledge.ladsp.com'], + requestedSize: {width: '300', height: '250'}, + allSlotsRequestedSizes: [{width: '300', height: '250'}], + sellerSignals: {signal: 'signal'}, + sellerTimeout: '500', + perBuyerSignals: {'https://fledge.ladsp.com': {signal: 'signal'}}, + perBuyerCurrencies: {'https://fledge.ladsp.com': 'USD'} + } + }] + }, + userSync: { + type: 'image', + url: 'https://cr-p31.ladsp.jp/cookiesender/31' + } + } + }; + const nativeServerResponse = { body: { seatbid: @@ -272,6 +321,11 @@ describe('LogicadAdapter', function () { const data = JSON.parse(request.data); expect(data.auctionId).to.equal('18fd8b8b0bd757'); + + // Protected Audience API flag + expect(data.bids[0]).to.have.property('ae'); + expect(data.bids[0].ae).to.equal(1); + expect(data.eids[0].source).to.equal('sharedid.org'); expect(data.eids[0].uids[0].id).to.equal('fakesharedid'); @@ -330,6 +384,13 @@ describe('LogicadAdapter', function () { expect(interpretedResponse[0].ttl).to.equal(serverResponse.body.seatbid[0].bid.ttl); expect(interpretedResponse[0].meta.advertiserDomains).to.equal(serverResponse.body.seatbid[0].bid.meta.advertiserDomains); + // Protected Audience API + const paapiRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; + const paapiInterpretedResponse = spec.interpretResponse(paapiServerResponse, paapiRequest); + expect(paapiInterpretedResponse).to.have.property('bids'); + expect(paapiInterpretedResponse).to.have.property('fledgeAuctionConfigs'); + expect(paapiInterpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal(paapiServerResponse.body.ext.fledgeAuctionConfigs[0]); + // native const nativeRequest = spec.buildRequests(nativeBidRequests, bidderRequest)[0]; const interpretedResponseForNative = spec.interpretResponse(nativeServerResponse, nativeRequest); diff --git a/test/spec/modules/loyalBidAdapter_spec .js b/test/spec/modules/loyalBidAdapter_spec .js new file mode 100644 index 00000000000..28e87fc7047 --- /dev/null +++ b/test/spec/modules/loyalBidAdapter_spec .js @@ -0,0 +1,375 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/loyalBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'loyal' + +describe('LoyalBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw' + }, + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, 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://us-east-1.loyal.app/pbjs'); + }); + + it('Returns general data 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', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + 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.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('object'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.an('array'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('object'); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, 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([], bidderRequest); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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/luceadBidAdapter_spec.js b/test/spec/modules/luceadBidAdapter_spec.js new file mode 100644 index 00000000000..72bc7cc2d6e --- /dev/null +++ b/test/spec/modules/luceadBidAdapter_spec.js @@ -0,0 +1,171 @@ +/* eslint-disable prebid/validate-imports,no-undef */ +import { expect } from 'chai'; +import { spec } from 'modules/luceadBidAdapter.js'; +import sinon from 'sinon'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import {deepClone} from 'src/utils.js'; +import * as ajax from 'src/ajax.js'; + +describe('Lucead Adapter', () => { + describe('inherited functions', function () { + it('exists and is a function', function () { + // noinspection JSCheckFunctionSignatures + const adapter = newBidder(spec); + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('utils functions', function () { + it('returns false', function () { + expect(spec.isDevEnv()).to.be.false; + }); + }); + + describe('isBidRequestValid', function () { + let bid; + beforeEach(function () { + bid = { + bidder: 'lucead', + params: { + placementId: '1', + }, + }; + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('onBidWon', function () { + let sandbox; + const bid = { foo: 'bar', creativeId: 'ssp:improve' }; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + it('should trigger impression pixel', function () { + sandbox.spy(ajax, 'fetch'); + spec.onBidWon(bid); + expect(ajax.fetch.args[0][0]).to.match(/report\/impression$/); + }); + + afterEach(function () { + sandbox.restore(); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + bidder: 'lucead', + adUnitCode: 'lucead_code', + bidId: 'abc1234', + sizes: [[1800, 1000], [640, 300]], + requestId: 'xyz654', + params: { + placementId: '123', + } + } + ]; + + const bidderRequest = { + bidderRequestId: '13aaa3df18bfe4', + bids: {} + }; + + it('should have a post method', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].method).to.equal('POST'); + }); + + it('should contains a request id equals to the bid id', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(request[0].data).bid_id).to.equal(bidRequests[0].bidId); + }); + + it('should have an url that contains sub keyword', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].url).to.match(/sub/); + }); + }); + + describe('interpretResponse', function () { + const serverResponse = { + body: { + 'bid_id': '2daf899fbe4c52', + 'request_id': '13aaa3df18bfe4', + 'ad': 'Ad', + 'ad_id': '3890677904', + 'cpm': 3.02, + 'currency': 'USD', + 'time': 1707257712095, + 'size': {'width': 300, 'height': 250}, + } + }; + + const bidRequest = {data: JSON.stringify({ + 'request_id': '13aaa3df18bfe4', + 'domain': '7cdb-2a02-8429-e4a0-1701-bc69-d51c-86e-b279.ngrok-free.app', + 'bid_id': '2daf899fbe4c52', + 'sizes': [[300, 250]], + 'media_types': {'banner': {'sizes': [[300, 250]]}}, + 'fledge_enabled': true, + 'enable_contextual': true, + 'enable_pa': true, + 'params': {'placementId': '1'}, + })}; + + it('should get correct bid response', function () { + const result = spec.interpretResponse(serverResponse, bidRequest); + + expect(Object.keys(result.bids[0])).to.have.members([ + 'requestId', + 'cpm', + 'width', + 'height', + 'currency', + 'ttl', + 'creativeId', + 'netRevenue', + 'ad', + 'meta', + ]); + }); + + it('should return bid empty response', function () { + const serverResponse = {body: {cpm: 0}}; + const bidRequest = {data: '{}'}; + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result.bids[0].ad).to.be.equal(''); + expect(result.bids[0].cpm).to.be.equal(0); + }); + + it('should add advertiserDomains', function () { + const bidRequest = {data: JSON.stringify({ + bidder: 'lucead', + params: { + placementId: '1', + } + })}; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(Object.keys(result.bids[0].meta)).to.include.members(['advertiserDomains']); + }); + + it('should support disabled contextual bids', function () { + const serverResponseWithDisabledContectual = deepClone(serverResponse); + serverResponseWithDisabledContectual.body.enable_contextual = false; + const result = spec.interpretResponse(serverResponseWithDisabledContectual, bidRequest); + expect(result.bids).to.be.null; + }); + + it('should support disabled Protected Audience', function () { + const serverResponseWithEnablePaFalse = deepClone(serverResponse); + serverResponseWithEnablePaFalse.body.enable_pa = false; + const result = spec.interpretResponse(serverResponseWithEnablePaFalse, bidRequest); + expect(result.fledgeAuctionConfigs).to.be.undefined; + }); + }); +}); diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index ae63f19f46b..397ee4a8577 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -3,7 +3,8 @@ import magniteAdapter, { getHostNameFromReferer, storage, rubiConf, - detectBrowserFromUa + detectBrowserFromUa, + callPrebidCacheHook } from '../../../modules/magniteAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; @@ -550,7 +551,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -724,7 +725,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -1137,6 +1138,39 @@ describe('magnite analytics adapter', function () { }); }); + it('should not use pbsBidId if the bid was client side cached', function () { + // bid response + let seatBidResponse = utils.deepClone(MOCK.BID_RESPONSE); + seatBidResponse.pbsBidId = 'do-not-use-me'; + + // Run auction + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // mock client side cache call + callPrebidCacheHook(() => {}, {}, seatBidResponse); + + events.emit(BID_RESPONSE, seatBidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + // emmit gpt events and bidWon + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + + events.emit(BID_WON, MOCK.BID_WON); + + // tick the event delay time plus processing delay + clock.tick(rubiConf.analyticsEventDelay + rubiConf.analyticsProcessDelay); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + // Expect the ids sent to server to use the original bidId not the pbsBidId thing + expect(message.auctions[0].adUnits[0].bids[0].bidId).to.equal(MOCK.BID_RESPONSE.requestId); + expect(message.bidsWon[0].bidId).to.equal(MOCK.BID_RESPONSE.requestId); + }); + [0, '0'].forEach(pbsParam => { it(`should generate new bidId if incoming pbsBidId is ${pbsParam}`, function () { // bid response @@ -1492,6 +1526,40 @@ describe('magnite analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + describe('when eventDispatcher is present', () => { + beforeEach(() => { + window.pbjs = window.pbjs || {}; + pbjs.rp = pbjs.rp || {}; + pbjs.rp.eventDispatcher = pbjs.rp.eventDispatcher || document.createElement('fakeElem'); + }); + + afterEach(() => { + delete pbjs.rp.eventDispatcher; + delete pbjs.rp; + }); + + it('should dispatch beforeSendingMagniteAnalytics if possible', () => { + pbjs.rp.eventDispatcher.addEventListener('beforeSendingMagniteAnalytics', (data) => { + data.detail.test = 'testData'; + }); + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('http://localhost:9999/event'); + + let message = JSON.parse(request.requestBody); + + const AnalyticsMessageWithCustomData = { + ...ANALYTICS_MESSAGE, + test: 'testData' + } + expect(message).to.deep.equal(AnalyticsMessageWithCustomData); + }); + }) + describe('when handling bid caching', () => { let auctionInits, bidRequests, bidResponses, bidsWon; beforeEach(function () { @@ -1673,6 +1741,79 @@ describe('magnite analytics adapter', function () { expect(message1.bidsWon).to.deep.equal([expectedMessage1]); }); }); + describe('cookieless', () => { + beforeEach(() => { + magniteAdapter.enableAnalytics({ + options: { + cookieles: undefined + } + }); + }) + afterEach(() => { + magniteAdapter.disableAnalytics(); + }) + it('should add sufix _cookieless to the wrapper.rule if ortb2.device.ext.cdep start with "treatment" or "control_2"', () => { + // Set the confs + config.setConfig({ + rubicon: { + wrapperName: '1001_general', + wrapperFamily: 'general', + rule_name: 'desktop-magnite.com', + } + }); + const auctionId = MOCK.AUCTION_INIT.auctionId; + + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.bidderRequests[0].ortb2.device.ext = { cdep: 'treatment' }; + // Run auction + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + [gptSlotRenderEnded0].forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); + events.emit(BID_WON, { ...MOCK.BID_WON, auctionId }); + clock.tick(rubiConf.analyticsEventDelay); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + expect(message.wrapper).to.deep.equal({ + name: '1001_general', + family: 'general', + rule: 'desktop-magnite.com_cookieless', + }); + }) + it('should add cookieless to the wrapper.rule if ortb2.device.ext.cdep start with "treatment" or "control_2"', () => { + // Set the confs + config.setConfig({ + rubicon: { + wrapperName: '1001_general', + wrapperFamily: 'general', + } + }); + const auctionId = MOCK.AUCTION_INIT.auctionId; + + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.bidderRequests[0].ortb2.device.ext = { cdep: 'control_2' }; + // Run auction + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + [gptSlotRenderEnded0].forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); + events.emit(BID_WON, { ...MOCK.BID_WON, auctionId }); + clock.tick(rubiConf.analyticsEventDelay); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + expect(message.wrapper).to.deep.equal({ + family: 'general', + name: '1001_general', + rule: 'cookieless', + }); + }); + }); }); describe('billing events integration', () => { diff --git a/test/spec/modules/mediabramaBidAdapter_spec.js b/test/spec/modules/mediabramaBidAdapter_spec.js new file mode 100644 index 00000000000..d7341e02f17 --- /dev/null +++ b/test/spec/modules/mediabramaBidAdapter_spec.js @@ -0,0 +1,256 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mediabramaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; +import * as utils from '../../../src/utils.js'; + +describe('MediaBramaBidAdapter', function () { + const bid = { + bidId: '23dc19818e5293', + bidder: 'mediabrama', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 24428, + } + }; + + 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://prebid.mediabrama.com/pbjs'); + }); + 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', '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.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', 'adFormat', 'sizes', 'schain', 'bidfloor'); + expect(placement.placementId).to.equal(24428); + expect(placement.bidId).to.equal('23dc19818e5293'); + expect(placement.adFormat).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + }); + + 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: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + 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', 'meta'); + expect(dataItem.requestId).to.equal('23dc19818e5293'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + 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 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 do nothing on getUserSyncs', 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://prebid.mediabrama.com/sync/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + }); + + describe('on bidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('should replace nurl for banner', function () { + const nurl = 'nurl/?ap=${' + 'AUCTION_PRICE}'; + const bid = { + 'bidderCode': 'mediabrama', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '5691dd18ba6ab6', + 'requestId': '23dc19818e5293', + 'transactionId': '948c716b-bf64-4303-bcf4-395c2f6a9770', + 'auctionId': 'a6b7c61f-15a9-481b-8f64-e859787e9c07', + 'mediaType': 'banner', + 'source': 'client', + 'ad': "
\n", + 'cpm': 0.61, + 'nurl': nurl, + 'creativeId': 'test', + 'currency': 'USD', + 'dealId': '', + 'meta': { + 'advertiserDomains': [], + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'name': 'mediabrama' + } + ] + } + }, + 'netRevenue': true, + 'ttl': 185, + 'metrics': {}, + 'adapterCode': 'mediabrama', + 'originalCpm': 0.61, + 'originalCurrency': 'USD', + 'responseTimestamp': 1668162732297, + 'requestTimestamp': 1668162732292, + 'bidder': 'mediabrama', + 'adUnitCode': 'div-prebid', + 'timeToRespond': 5, + 'pbLg': '0.50', + 'pbMg': '0.60', + 'pbHg': '0.61', + 'pbAg': '0.61', + 'pbDg': '0.61', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'mediabrama', + 'hb_adid': '5691dd18ba6ab6', + 'hb_pb': '0.61', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '' + }, + 'status': 'rendered', + 'params': [ + { + 'placementId': 24428 + } + ] + }; + spec.onBidWon(bid); + expect(bid.nurl).to.deep.equal('nurl/?ap=0.61'); + }); + }); +}); diff --git a/test/spec/modules/mediafilterRtdProvider_spec.js b/test/spec/modules/mediafilterRtdProvider_spec.js new file mode 100644 index 00000000000..3395c7be691 --- /dev/null +++ b/test/spec/modules/mediafilterRtdProvider_spec.js @@ -0,0 +1,147 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; + +import { + MediaFilter, + MEDIAFILTER_EVENT_TYPE, + MEDIAFILTER_BASE_URL +} from '../../../modules/mediafilterRtdProvider.js'; + +describe('The Media Filter RTD module', function () { + describe('register()', function() { + let submoduleSpy, generateInitHandlerSpy; + + beforeEach(function () { + submoduleSpy = sinon.spy(hook, 'submodule'); + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + }); + + afterEach(function () { + submoduleSpy.restore(); + generateInitHandlerSpy.restore(); + }); + + it('should register and call the submodule function(s)', function () { + MediaFilter.register(); + + expect(submoduleSpy.calledOnceWithExactly('realTimeData', sinon.match.object)).to.be.true; + expect(submoduleSpy.called).to.be.true; + expect(generateInitHandlerSpy.called).to.be.true; + }); + }); + + describe('setup()', function() { + let setupEventListenerSpy, setupScriptSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + setupScriptSpy = sinon.spy(MediaFilter, 'setupScript'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + setupScriptSpy.restore(); + }); + + it('should call setupEventListener and setupScript function(s)', function() { + MediaFilter.setup({ configurationHash: 'abc123' }); + + expect(setupEventListenerSpy.called).to.be.true; + expect(setupScriptSpy.called).to.be.true; + }); + }); + + describe('setupEventListener()', function() { + let setupEventListenerSpy, addEventListenerSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + addEventListenerSpy.restore(); + }); + + it('should call addEventListener function(s)', function() { + MediaFilter.setupEventListener(); + expect(addEventListenerSpy.called).to.be.true; + expect(addEventListenerSpy.calledWith('message', sinon.match.func)).to.be.true; + }); + }); + + describe('generateInitHandler()', function() { + let generateInitHandlerSpy, setupMock, logErrorSpy; + + beforeEach(function() { + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + setupMock = sinon.stub(MediaFilter, 'setup').throws(new Error('Mocked error!')); + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + generateInitHandlerSpy.restore(); + setupMock.restore(); + logErrorSpy.restore(); + }); + + it('should handle errors in the catch block when setup throws an error', function() { + const initHandler = MediaFilter.generateInitHandler(); + initHandler({}); + + expect(logErrorSpy.calledWith('Error in initialization: Mocked error!')).to.be.true; + }); + }); + + describe('generateEventHandler()', function() { + let generateEventHandlerSpy, eventsEmitSpy; + + beforeEach(function() { + generateEventHandlerSpy = sinon.spy(MediaFilter, 'generateEventHandler'); + eventsEmitSpy = sinon.spy(events, 'emit'); + }); + + afterEach(function() { + generateEventHandlerSpy.restore(); + eventsEmitSpy.restore(); + }); + + it('should emit a billable event when the event type matches', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash) + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': sinon.match.string, + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + })).to.be.true; + }); + + it('should not emit a billable event when the event type does not match', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: 'differentEventType' + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.called).to.be.false; + }); + }); +}); diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index e77af544429..6e58217b3d3 100644 --- a/test/spec/modules/mediagoBidAdapter_spec.js +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -1,5 +1,17 @@ import { expect } from 'chai'; -import { spec } from 'modules/mediagoBidAdapter.js'; +import { + spec, + getPmgUID, + storage, + getPageTitle, + getPageDescription, + getPageKeywords, + getConnectionDownLink, + THIRD_PARTY_COOKIE_ORIGIN, + COOKIE_KEY_MGUID, + getCurrentTimeToUTCString +} from 'modules/mediagoBidAdapter.js'; +import * as utils from 'src/utils.js'; describe('mediago:BidAdapterTests', function () { let bidRequestData = { @@ -11,12 +23,49 @@ describe('mediago:BidAdapterTests', function () { bidder: 'mediago', params: { token: '85a6b01e41ac36d49744fad726e3655d', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://trace.mediago.io', bidfloor: 0.01, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA' + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name' + }, + pbadslot: '/12345/my-gpt-tag-0' + } + } + } }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left' + } + }, + ortb2: { + site: { + cat: ['IAB2'], + keywords: 'power tools, drills, tools=industrial', + content: { + keywords: 'video, source=streaming' + }, + }, + user: { + ext: { + data: {} + } + } }, adUnitCode: 'regular_iframe', transactionId: '7b26fdae-96e6-4c35-a18b-218dda11397d', @@ -27,9 +76,83 @@ describe('mediago:BidAdapterTests', function () { src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, - bidderWinsCount: 0, - }, + bidderWinsCount: 0 + } ], + gdprConsent: { + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + }, + userId: { + tdid: 'sample-userid', + uid2: { id: 'sample-uid2-value' }, + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid', + pubProvidedId: [ + { + source: 'puburl.com', + uids: [ + { + id: 'pubid2', + atype: 1, + ext: { + stype: 'ppuid' + } + } + ] + }, + { + source: 'puburl2.com', + uids: [ + { + id: 'pubid2' + }, + { + id: 'pubid2-123' + } + ] + } + ] + }, + userIdAsEids: [ + { + source: 'adserver.org', + uids: [{ id: 'sample-userid' }] + }, + { + source: 'criteo.com', + uids: [{ id: 'sample-criteo-userid' }] + }, + { + source: 'netid.de', + uids: [{ id: 'sample-netId-userid' }] + }, + { + source: 'liveramp.com', + uids: [{ id: 'sample-idl-userid' }] + }, + { + source: 'uidapi.com', + uids: [{ id: 'sample-uid2-value' }] + }, + { + source: 'puburl.com', + uids: [{ id: 'pubid1' }] + }, + { + source: 'puburl2.com', + uids: [{ id: 'pubid2' }, { id: 'pubid2-123' }] + } + ] }; let request = []; @@ -38,8 +161,8 @@ describe('mediago:BidAdapterTests', function () { spec.isBidRequestValid({ bidder: 'mediago', params: { - token: ['85a6b01e41ac36d49744fad726e3655d'], - }, + token: ['85a6b01e41ac36d49744fad726e3655d'] + } }) ).to.equal(true); }); @@ -50,11 +173,54 @@ describe('mediago:BidAdapterTests', function () { expect(req_data.imp).to.have.lengthOf(1); }); + describe('mediago: buildRequests', function() { + describe('getPmgUID function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(utils, 'generateUUID').returns('new-uuid'); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should generate new UUID and set cookie if not exists', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => null); + const uid = getPmgUID(); + expect(uid).to.equal('new-uuid'); + expect(storage.setCookie.calledOnce).to.be.true; + }); + + it('should return existing UUID from cookie', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => 'existing-uuid'); + const uid = getPmgUID(); + expect(uid).to.equal('existing-uuid'); + expect(storage.setCookie.called).to.be.false; + }); + + it('should not set new UUID when cookies are not enabled', () => { + storage.cookiesAreEnabled.callsFake(() => false); + storage.getCookie.callsFake(() => null); + getPmgUID(); + expect(storage.setCookie.calledOnce).to.be.false; + }); + }) + }); + it('mediago:validate_response_params', function () { - let adm = ""; + let adm = + ''; let temp = '%3Cscr'; temp += 'ipt%3E'; - temp += '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; + temp += + '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; temp += '%3B%3C%2Fscri'; temp += 'pt%3E'; adm += decodeURIComponent(temp); @@ -72,13 +238,13 @@ describe('mediago:BidAdapterTests', function () { cid: '1339145', crid: 'ff32b6f9b3bbc45c00b78b6674a2952e', w: 300, - h: 250, - }, - ], - }, + h: 250 + } + ] + } ], - cur: 'USD', - }, + cur: 'USD' + } }; let bids = spec.interpretResponse(serverResponse); @@ -94,4 +260,324 @@ describe('mediago:BidAdapterTests', function () { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); + + describe('mediago: getUserSyncs', function() { + const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; + const IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }; + const IFRAME_DISABLED = { + iframeEnabled: false, + pixelEnabled: false, + }; + const GDPR_CONSENT = { + consentString: 'gdprConsentString', + gdprApplies: true + }; + const USP_CONSENT = { + consentString: 'uspConsentString' + } + + let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; + syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; + const expectedIframeSyncs = [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + + it('should return nothing if iframe is disabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.be.undefined; + }); + + it('should do userSyncs if iframe is enabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.deep.equal(expectedIframeSyncs); + }); + }); +}); + +describe('mediago Bid Adapter Tests', function () { + describe('buildRequests', () => { + describe('getPageTitle function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document title if available', function() { + const fakeTopDocument = { + title: 'Top Document Title', + querySelector: () => ({ content: 'Top Document Title test' }) + }; + const fakeTopWindow = { + document: fakeTopDocument + }; + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the content of top og:title meta tag if title is empty', function() { + const ogTitleContent = 'Top OG Title Content'; + const fakeTopWindow = { + document: { + title: '', + querySelector: sandbox.stub().withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }) + } + }; + + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return the document title if no og:title meta tag is present', function() { + document.title = 'Test Page Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + + const result = getPageTitle({ top: undefined }); + expect(result).to.equal('Test Page Title'); + }); + + it('should return the content of og:title meta tag if present', function() { + document.title = ''; + const ogTitleContent = 'Top OG Title Content'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return an empty string if no title or og:title meta tag is found', function() { + document.title = ''; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(''); + }); + + it('should handle exceptions when accessing top.document and fallback to current document', function() { + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const ogTitleContent = 'Current OG Title Content'; + document.title = 'Current Document Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle(fakeWindow); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document description if available', function() { + const descriptionContent = 'Top Document Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[name="description"]').returns({ content: descriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(descriptionContent); + }); + + it('should return the top document og:description if description is not present', function() { + const ogDescriptionContent = 'Top OG Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(ogDescriptionContent); + }); + + it('should return the current document description if top document is not accessible', function() { + const descriptionContent = 'Current Document Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="description"]').returns({ content: descriptionContent }) + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(descriptionContent); + }); + + it('should return the current document og:description if description is not present and top document is not accessible', function() { + const ogDescriptionContent = 'Current OG Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }); + + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(ogDescriptionContent); + }); + }); + + describe('getPageKeywords function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document keywords if available', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + const fakeTopDocument = { + querySelector: sandbox.stub() + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + + const result = getPageKeywords({ top: fakeTopWindow }); + expect(result).to.equal(keywordsContent); + }); + + it('should return the current document keywords if top document is not accessible', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }); + + // æ¨Ąæ‹ŸéĄļåą‚įĒ—åŖčŽŋ问åŧ‚常 + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + + const result = getPageKeywords(fakeWindow); + expect(result).to.equal(keywordsContent); + }); + + it('should return an empty string if no keywords meta tag is found', function() { + sandbox.stub(document, 'querySelector').withArgs('meta[name="keywords"]').returns(null); + + const result = getPageKeywords(); + expect(result).to.equal(''); + }); + }); + describe('getConnectionDownLink function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the downlink value as a string if available', function() { + const downlinkValue = 2.5; + const fakeNavigator = { + connection: { + downlink: downlinkValue + } + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.equal(downlinkValue.toString()); + }); + + it('should return undefined if downlink is not available', function() { + const fakeNavigator = { + connection: {} + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should return undefined if connection is not available', function() { + const fakeNavigator = {}; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should handle cases where navigator is not defined', function() { + const result = getConnectionDownLink({}); + expect(result).to.be.undefined; + }); + }); + + describe('getUserSyncs with message event listener', function() { + function messageHandler(event) { + if (!event.data || event.origin !== THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + window.removeEventListener('message', messageHandler, true); + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + } + + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(window, 'removeEventListener'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should set a cookie when a valid message is received', () => { + const fakeEvent = { + data: { optout: '', mguid: '12345' }, + origin: THIRD_PARTY_COOKIE_ORIGIN, + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.calledOnce).to.be.true; + expect(window.removeEventListener.calledWith('message', messageHandler, true)).to.be.true; + expect(storage.setCookie.calledWith(COOKIE_KEY_MGUID, '12345', sinon.match.string)).to.be.true; + }); + it('should not do anything when an invalid message is received', () => { + const fakeEvent = { + data: null, + origin: 'http://invalid-origin.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.notCalled).to.be.true; + expect(window.removeEventListener.notCalled).to.be.true; + expect(storage.setCookie.notCalled).to.be.true; + }); + }); + }); }); diff --git a/test/spec/modules/mediaimpactBidAdapter_spec.js b/test/spec/modules/mediaimpactBidAdapter_spec.js new file mode 100644 index 00000000000..3d706e59c3f --- /dev/null +++ b/test/spec/modules/mediaimpactBidAdapter_spec.js @@ -0,0 +1,336 @@ +import {expect} from 'chai'; +import {spec, ENDPOINT_PROTOCOL, ENDPOINT_DOMAIN, ENDPOINT_PATH} from 'modules/mediaimpactBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'mediaimpact'; + +describe('MediaimpactAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.be.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + let validRequest = { + 'params': { + 'unitId': 123 + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(true); + }); + + it('should return true when required params is srting', function () { + let validRequest = { + 'params': { + 'unitId': '456' + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let validRequest = { + 'params': { + 'unknownId': 123 + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when required params is 0', function () { + let validRequest = { + 'params': { + 'unitId': 0 + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let validEndpoint = ENDPOINT_PROTOCOL + '://' + ENDPOINT_DOMAIN + ENDPOINT_PATH + '?tag=123,456&partner=777&sizes=300x250|300x600,728x90,300x250&referer=https%3A%2F%2Ftest.domain'; + + let validRequest = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'unitId': 123 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e' + }, + { + 'bidder': BIDDER_CODE, + 'params': { + 'unitId': '456' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '22aidtbx5eabd9' + }, + { + 'bidder': BIDDER_CODE, + 'params': { + 'partnerId': 777 + }, + 'adUnitCode': 'partner-code-3', + 'sizes': [[300, 250]], + 'bidId': '5d4531d5a6c013' + } + ]; + + let bidderRequest = { + refererInfo: { + page: 'https://test.domain' + } + }; + + it('bidRequest HTTP method', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('bidRequest url', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.url).to.equal(validEndpoint); + }); + + it('bidRequest data', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload[0].unitId).to.equal(123); + expect(payload[0].sizes).to.deep.equal([[300, 250], [300, 600]]); + expect(payload[0].bidId).to.equal('30b31c1838de1e'); + expect(payload[1].unitId).to.equal(456); + expect(payload[1].sizes).to.deep.equal([[728, 90]]); + expect(payload[1].bidId).to.equal('22aidtbx5eabd9'); + expect(payload[2].partnerId).to.equal(777); + expect(payload[2].sizes).to.deep.equal([[300, 250]]); + expect(payload[2].bidId).to.equal('5d4531d5a6c013'); + }); + }); + + describe('joinSizesToString', function () { + it('success convert sizes list to string', function () { + const sizesStr = spec.joinSizesToString([[300, 250], [300, 600]]); + expect(sizesStr).to.equal('300x250|300x600'); + }); + }); + + describe('interpretResponse', function () { + const bidRequest = { + 'method': 'POST', + 'url': ENDPOINT_PROTOCOL + '://' + ENDPOINT_DOMAIN + ENDPOINT_PATH + '?tag=123,456&partner=777code=adunit-code-1,adunit-code-2,partner-code-3&bid=30b31c1838de1e,22aidtbx5eabd9,5d4531d5a6c013&sizes=300x250|300x600,728x90,300x250&referer=https%3A%2F%2Ftest.domain', + 'data': '[{"unitId": 13144370,"adUnitCode": "div-gpt-ad-1460505748561-0","sizes": [[300, 250], [300, 600]],"bidId": "2bdcb0b203c17d","referer": "https://test.domain/index.html"},' + + '{"unitId": 13144370,"adUnitCode":"div-gpt-ad-1460505748561-1","sizes": [[768, 90]],"bidId": "3dc6b8084f91a8","referer": "https://test.domain/index.html"},' + + '{"unitId": 0,"partnerId": 777,"adUnitCode":"div-gpt-ad-1460505748561-2","sizes": [[300, 250]],"bidId": "5d4531d5a6c013","referer": "https://test.domain/index.html"}]' + }; + + const bidResponse = { + body: { + 'div-gpt-ad-1460505748561-0': + { + 'ad': '
ad
', + 'width': 300, + 'height': 250, + 'creativeId': '8:123456', + 'adomain': [ + 'test.domain' + ], + 'syncs': [ + {'type': 'image', 'url': 'https://test.domain/tracker_1.gif'}, + {'type': 'image', 'url': 'https://test.domain/tracker_2.gif'}, + {'type': 'image', 'url': 'https://test.domain/tracker_3.gif'} + ], + 'winNotification': [ + { + 'method': 'POST', + 'path': '/hb/bid_won?test=1', + 'data': { + 'ad': [ + {'dsp': 8, 'id': 800008, 'cost': 1.0e-5, 'nurl': 'https://test.domain/'} + ], + 'unit_id': 1234, + 'site_id': 123 + } + } + ], + 'cpm': 0.01, + 'currency': 'USD', + 'netRevenue': true + } + }, + headers: {} + }; + + it('result is correct', function () { + const result = spec.interpretResponse(bidResponse, bidRequest); + expect(result[0].requestId).to.equal('2bdcb0b203c17d'); + expect(result[0].cpm).to.equal(0.01); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal('8:123456'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].ttl).to.equal(60); + expect(result[0].meta.advertiserDomains).to.deep.equal(['test.domain']); + expect(result[0].winNotification[0]).to.deep.equal({'method': 'POST', 'path': '/hb/bid_won?test=1', 'data': {'ad': [{'dsp': 8, 'id': 800008, 'cost': 1.0e-5, 'nurl': 'https://test.domain/'}], 'unit_id': 1234, 'site_id': 123}}); + }); + }); + + describe('adResponse', function () { + const bid = { + 'unitId': 13144370, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2bdcb0b203c17d', + 'referer': 'https://test.domain/index.html' + }; + const ad = { + 'ad': '
ad
', + 'width': 300, + 'height': 250, + 'creativeId': '8:123456', + 'syncs': [], + 'winNotification': [], + 'cpm': 0.01, + 'currency': 'USD', + 'netRevenue': true, + 'adomain': [ + 'test.domain' + ], + }; + + it('fill ad for response', function () { + const result = spec.adResponse(bid, ad); + expect(result.requestId).to.equal('2bdcb0b203c17d'); + expect(result.cpm).to.equal(0.01); + expect(result.width).to.equal(300); + expect(result.height).to.equal(250); + expect(result.creativeId).to.equal('8:123456'); + expect(result.currency).to.equal('USD'); + expect(result.ttl).to.equal(60); + expect(result.meta.advertiserDomains).to.deep.equal(['test.domain']); + }); + }); + + describe('onBidWon', function () { + const bid = { + winNotification: [ + { + 'method': 'POST', + 'path': '/hb/bid_won?test=1', + 'data': { + 'ad': [ + {'dsp': 8, 'id': 800008, 'cost': 0.01, 'nurl': 'http://test.domain/'} + ], + 'unit_id': 1234, + 'site_id': 123 + } + } + ] + }; + + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'postRequest') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('calls mediaimpact callback endpoint', () => { + const result = spec.onBidWon(bid); + expect(result).to.equal(true); + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(ENDPOINT_PROTOCOL + '://' + ENDPOINT_DOMAIN + '/hb/bid_won?test=1'); + expect(ajaxStub.firstCall.args[1]).to.deep.equal(JSON.stringify(bid.winNotification[0].data)); + }); + }); + + describe('getUserSyncs', function () { + const bidResponse = [{ + body: { + 'div-gpt-ad-1460505748561-0': + { + 'ad': '
ad
', + 'width': 300, + 'height': 250, + 'creativeId': '8:123456', + 'adomain': [ + 'test.domain' + ], + 'syncs': [ + {'type': 'image', 'link': 'https://test.domain/tracker_1.gif'}, + {'type': 'image', 'link': 'https://test.domain/tracker_2.gif'}, + {'type': 'image', 'link': 'https://test.domain/tracker_3.gif'} + ], + 'winNotification': [ + { + 'method': 'POST', + 'path': '/hb/bid_won?test=1', + 'data': { + 'ad': [ + {'dsp': 8, 'id': 800008, 'cost': 1.0e-5, 'nurl': 'https://test.domain/'} + ], + 'unit_id': 1234, + 'site_id': 123 + } + } + ], + 'cpm': 0.01, + 'currency': 'USD', + 'netRevenue': true + } + }, + headers: {} + }]; + + 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 image sync when only image is enabled where gdprConsent is undefined', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true + }; + + const gdprConsent = undefined; + let syncs = spec.getUserSyncs(syncOptions, bidResponse, gdprConsent); + expect(syncs.length).to.equal(3); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://test.domain/tracker_1.gif'); + }); + + it('should register image sync when only image is enabled where gdprConsent is defined', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'someString', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + + let syncs = spec.getUserSyncs(syncOptions, bidResponse, gdprConsent); + expect(syncs.length).to.equal(3); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://test.domain/tracker_1.gif?gdpr=1&gdpr_consent=someString'); + }); + }); +}); diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index bb90eded230..4a221e97444 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -915,24 +915,24 @@ let VALID_BID_REQUEST = [{ cid: '8CUV090' } }, - VALID_PARAMS_AAX = { - bidder: 'aax', + VALID_PARAMS_TS = { + bidder: 'trustedstack', params: { - cid: 'AAXG123' + cid: 'TS012345' } }, PARAMS_MISSING = { bidder: 'medianet', }, - PARAMS_MISSING_AAX = { - bidder: 'aax', + PARAMS_MISSING_TS = { + bidder: 'trustedstack', }, PARAMS_WITHOUT_CID = { bidder: 'medianet', params: {} }, - PARAMS_WITHOUT_CID_AAX = { - bidder: 'aax', + PARAMS_WITHOUT_CID_TS = { + bidder: 'trustedstack', params: {} }, PARAMS_WITH_INTEGER_CID = { @@ -941,8 +941,8 @@ let VALID_BID_REQUEST = [{ cid: 8867587 } }, - PARAMS_WITH_INTEGER_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_INTEGER_CID_TS = { + bidder: 'trustedstack', params: { cid: 8867587 } @@ -953,8 +953,8 @@ let VALID_BID_REQUEST = [{ cid: '' } }, - PARAMS_WITH_EMPTY_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_EMPTY_CID_TS = { + bidder: 'trustedstack', params: { cid: '' } @@ -1311,6 +1311,118 @@ let VALID_BID_REQUEST = [{ } }], 'tmax': 3000, + }, + VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2 = { + ortb2: { + regs: { + gpp: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + gpp_sid: [5, 7] + } + }, + 'timeout': 3000, + refererInfo: { + referer: 'http://media.net/prebidtest', + stack: ['http://media.net/prebidtest'], + page: 'http://media.net/page', + domain: 'media.net', + topmostLocation: 'http://media.net/topmost', + reachedTop: true + } + }, + VALID_PAYLOAD_FOR_GPP_ORTB2 = { + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'topMostLocation': 'http://media.net/topmost', + 'isTop': true + }, + 'ext': { + 'customer_id': 'customer_id', + 'prebid_version': $$PREBID_GLOBAL$$.version, + 'gdpr_applies': false, + 'usp_applies': false, + 'coppa_applies': false, + 'screen': { + 'w': 1000, + 'h': 1000 + } + }, + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'imp': [{ + 'id': '28f8f8130a583e', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + ortb2Imp: VALID_BID_REQUEST[0].ortb2Imp, + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-0', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 250 + }], + 'all': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }, { + 'id': '3f97ca71b1e5c2', + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + ortb2Imp: VALID_BID_REQUEST[1].ortb2Imp, + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-123', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 251 + }], + 'all': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }], + 'ortb2': { + 'regs': { + 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'gpp_sid': [5, 7], + } + }, + 'tmax': config.getConfig('bidderTimeout') }; describe('Media.net bid adapter', function () { let sandbox; @@ -1393,6 +1505,11 @@ describe('Media.net bid adapter', function () { expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_FOR_GDPR); }); + it('should have gpp params in ortb2', function () { + let bidReq = spec.buildRequests(VALID_BID_REQUEST, VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_FOR_GPP_ORTB2); + }); + it('should parse params for native request', function () { let bidReq = spec.buildRequests(VALID_NATIVE_BID_REQUEST, VALID_AUCTIONDATA); expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_NATIVE); @@ -1666,34 +1783,34 @@ describe('Media.net bid adapter', function () { }); }); - describe('isBidRequestValid aax', function () { + describe('isBidRequestValid trustedstack', function () { it('should accept valid bid params', function () { - let isValid = spec.isBidRequestValid(VALID_PARAMS_AAX); + let isValid = spec.isBidRequestValid(VALID_PARAMS_TS); expect(isValid).to.equal(true); }); it('should reject bid if cid is not present', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is not a string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is a empty string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_TS); expect(isValid).to.equal(false); }); it('should have missing params', function () { - let isValid = spec.isBidRequestValid(PARAMS_MISSING_AAX); + let isValid = spec.isBidRequestValid(PARAMS_MISSING_TS); expect(isValid).to.equal(false); }); }); - describe('interpretResponse aax', function () { + describe('interpretResponse trustedstack', function () { it('should not push response if no-bid', function () { let validBids = []; let bids = spec.interpretResponse(SERVER_RESPONSE_NOBID, []); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index b2fbcb1ba59..cdeae38aa19 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; import {spec} from 'modules/mediasquareBidAdapter.js'; +import { server } from 'test/mocks/xhr.js'; describe('MediaSquare bid adapter tests', function () { var DEFAULT_PARAMS = [{ @@ -100,10 +101,35 @@ describe('MediaSquare bid adapter tests', function () { 'adomain': ['test.com'], 'context': 'instream', 'increment': 1.0, + 'ova': 'cleared', + 'dsa': { + 'behalf': 'some-behalf', + 'paid': 'some-paid', + 'transparency': [{ + 'domain': 'test.com', + 'dsaparams': [1, 2, 3] + }], + 'adrender': 1 + } }], }}; const DEFAULT_OPTIONS = { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + } + } + }, gdprConsent: { gdprApplies: true, consentString: 'BOzZdA0OzZdA0AGABBENDJ-AAAAvh7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__79__3z3_9pxP78k89r7337Mw_v-_v-b7JCPN_Y3v-8Kg', @@ -142,10 +168,12 @@ describe('MediaSquare bid adapter tests', function () { expect(requestContent.codes[0]).to.have.property('mediatypes').exist; expect(requestContent.codes[0]).to.have.property('floor').exist; expect(requestContent.codes[0].floor).to.deep.equal({}); + expect(requestContent).to.have.property('dsa'); const requestfloor = spec.buildRequests(FLOORS_PARAMS, DEFAULT_OPTIONS); const responsefloor = JSON.parse(requestfloor.data); expect(responsefloor.codes[0]).to.have.property('floor').exist; expect(responsefloor.codes[0].floor).to.have.property('300x250').and.to.have.property('floor').and.to.equal(1); + expect(responsefloor.codes[0].floor).to.have.property('*'); }); it('Verify parse response', function () { @@ -170,9 +198,11 @@ describe('MediaSquare bid adapter tests', function () { expect(bid.mediasquare.increment).to.exist; expect(bid.mediasquare.increment).to.equal(1.0); expect(bid.mediasquare.code).to.equal([DEFAULT_PARAMS[0].params.owner, DEFAULT_PARAMS[0].params.code].join('/')); + expect(bid.mediasquare.ova).to.exist.and.to.equal('cleared'); expect(bid.meta).to.exist; expect(bid.meta.advertiserDomains).to.exist; expect(bid.meta.advertiserDomains).to.have.lengthOf(1); + expect(bid.meta.dsa).to.exist; }); it('Verifies match', function () { const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); @@ -208,6 +238,11 @@ describe('MediaSquare bid adapter tests', function () { const response = spec.interpretResponse(BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('increment').exist; + expect(message).to.have.property('increment').and.to.equal('1'); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); it('Verifies user sync without cookie in bid response', function () { var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); diff --git a/test/spec/modules/mgidRtdProvider_spec.js b/test/spec/modules/mgidRtdProvider_spec.js index 4f70b4d8b7c..996875649b6 100644 --- a/test/spec/modules/mgidRtdProvider_spec.js +++ b/test/spec/modules/mgidRtdProvider_spec.js @@ -1,16 +1,14 @@ import { mgidSubmodule, storage } from '../../../modules/mgidRtdProvider.js'; import {expect} from 'chai'; import * as refererDetection from '../../../src/refererDetection'; +import {server} from '../../mocks/xhr.js'; describe('Mgid RTD submodule', () => { - let server; let clock; let getRefererInfoStub; let getDataFromLocalStorageStub; beforeEach(() => { - server = sinon.fakeServer.create(); - clock = sinon.useFakeTimers(); getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); @@ -22,7 +20,6 @@ describe('Mgid RTD submodule', () => { }); afterEach(() => { - server.restore(); clock.restore(); getRefererInfoStub.restore(); getDataFromLocalStorageStub.restore(); @@ -309,7 +306,6 @@ describe('Mgid RTD submodule', () => { server.requests[0].respond( 204, {'Content-Type': 'application/json'}, - '{}' ); assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); diff --git a/test/spec/modules/mgidXBidAdapter_spec.js b/test/spec/modules/mgidXBidAdapter_spec.js index 14619e9c0e1..e0b1e1a84e9 100644 --- a/test/spec/modules/mgidXBidAdapter_spec.js +++ b/test/spec/modules/mgidXBidAdapter_spec.js @@ -6,7 +6,6 @@ import { config } from '../../../src/config'; import { USERSYNC_DEFAULT_CONFIG } from '../../../src/userSync'; const bidder = 'mgidX' -const adUrl = 'https://us-east-x.mgid.com/pbjs'; describe('MGIDXBidAdapter', function () { const bids = [ @@ -19,6 +18,7 @@ describe('MGIDXBidAdapter', function () { } }, params: { + region: 'eu', placementId: 'testBanner', } }, @@ -56,6 +56,7 @@ describe('MGIDXBidAdapter', function () { } }, params: { + region: 'eu', placementId: 'testNative', } } @@ -76,7 +77,10 @@ describe('MGIDXBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { referer: 'https://test.com' } @@ -105,8 +109,16 @@ describe('MGIDXBidAdapter', function () { expect(serverRequest.method).to.equal('POST'); }); - it('Returns valid URL', function () { - expect(serverRequest.url).to.equal(adUrl); + it('Returns valid EU URL', function () { + bids[0].params.region = 'eu'; + serverRequest = spec.buildRequests(bids, bidderRequest); + expect(serverRequest.url).to.equal('https://eu.mgid.com/pbjs'); + }); + + it('Returns valid EAST URL', function () { + bids[0].params.region = 'other'; + serverRequest = spec.buildRequests(bids, bidderRequest); + expect(serverRequest.url).to.equal('https://us-east-x.mgid.com/pbjs'); }); it('Returns general data valid', function () { @@ -131,7 +143,7 @@ describe('MGIDXBidAdapter', function () { expect(data.host).to.be.a('string'); expect(data.page).to.be.a('string'); expect(data.coppa).to.be.a('number'); - expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.be.a('object'); expect(data.ccpa).to.be.a('string'); expect(data.tmax).to.be.a('number'); expect(data.placements).to.have.lengthOf(3); @@ -172,8 +184,10 @@ describe('MGIDXBidAdapter', function () { serverRequest = spec.buildRequests(bids, 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.gdpr).to.be.a('object'); + expect(data.gdpr).to.have.property('consentString'); + expect(data.gdpr).to.not.have.property('vendorData'); + expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); expect(data.ccpa).to.not.exist; delete bidderRequest.gdprConsent; }); @@ -188,12 +202,6 @@ describe('MGIDXBidAdapter', function () { 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([], bidderRequest); - let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; - }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/microadBidAdapter_spec.js b/test/spec/modules/microadBidAdapter_spec.js index bd6d04a6312..9eb36d2fa6c 100644 --- a/test/spec/modules/microadBidAdapter_spec.js +++ b/test/spec/modules/microadBidAdapter_spec.js @@ -382,6 +382,196 @@ describe('microadBidAdapter', () => { }) }); }) + + describe('should send gpid', () => { + it('from gpid', () => { + const bidRequest = Object.assign({}, bidRequestTemplate, { + ortb2Imp: { + ext: { + tid: 'transaction-id', + gpid: '1111/2222', + data: { + pbadslot: '3333/4444' + } + } + } + }); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + gpid: '1111/2222', + pbadslot: '3333/4444' + }) + ); + }) + }) + + it('from pbadslot', () => { + const bidRequest = Object.assign({}, bidRequestTemplate, { + ortb2Imp: { + ext: { + tid: 'transaction-id', + data: { + pbadslot: '3333/4444' + } + } + } + }); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + gpid: '3333/4444', + pbadslot: '3333/4444' + }) + ); + }) + }) + }) + + const notGettingGpids = { + 'they are not existing': bidRequestTemplate, + 'they are blank': { + ortb2Imp: { + ext: { + tid: 'transaction-id', + gpid: '', + data: { + pbadslot: '' + } + } + } + } + } + + Object.entries(notGettingGpids).forEach(([testTitle, param]) => { + it(`should not send gpid because ${testTitle}`, () => { + const bidRequest = Object.assign({}, bidRequestTemplate, param); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + }) + ); + expect(request.data.gpid).to.be.undefined; + expect(request.data.pbadslot).to.be.undefined; + }) + }) + }) + + it('should send adservname', () => { + const bidRequest = Object.assign({}, bidRequestTemplate, { + ortb2Imp: { + ext: { + tid: 'transaction-id', + data: { + adserver: { + name: 'gam' + } + } + } + } + }); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + adservname: 'gam' + }) + ); + }) + }) + + const notGettingAdservnames = { + 'it is not existing': bidRequestTemplate, + 'it is blank': { + ortb2Imp: { + ext: { + tid: 'transaction-id', + data: { + adserver: { + name: '' + } + } + } + } + } + } + + Object.entries(notGettingAdservnames).forEach(([testTitle, param]) => { + it(`should not send adservname because ${testTitle}`, () => { + const bidRequest = Object.assign({}, bidRequestTemplate, param); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + }) + ); + expect(request.data.adservname).to.be.undefined; + }) + }) + }) + + it('should send adservadslot', () => { + const bidRequest = Object.assign({}, bidRequestTemplate, { + ortb2Imp: { + ext: { + tid: 'transaction-id', + data: { + adserver: { + adslot: '/1111/home' + } + } + } + } + }); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + adservadslot: '/1111/home' + }) + ); + }) + }) + + const notGettingAdservadslots = { + 'it is not existing': bidRequestTemplate, + 'it is blank': { + ortb2Imp: { + ext: { + tid: 'transaction-id', + data: { + adserver: { + adslot: '' + } + } + } + } + } + } + + Object.entries(notGettingAdservadslots).forEach(([testTitle, param]) => { + it(`should not send adservadslot because ${testTitle}`, () => { + const bidRequest = Object.assign({}, bidRequestTemplate, param); + const requests = spec.buildRequests([bidRequest], bidderRequest) + requests.forEach(request => { + expect(request.data).to.deep.equal( + Object.assign({}, expectedResultTemplate, { + cbt: request.data.cbt, + }) + ); + expect(request.data.adservadslot).to.be.undefined; + }) + }) + }) }); describe('interpretResponse', () => { diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index 48f694bc79d..d5d6cdc5449 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -178,6 +178,16 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[1].mediaType).to.equal(BANNER) }); + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + it('should respect syncEnabled option', function() { config.setConfig({ userSync: { @@ -291,6 +301,22 @@ describe('minutemediaAdapter', function () { expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); }); + it('should not send the gpp param if gppConsent is false in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: false}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gpp'); + expect(request.data.params).to.not.have.property('gpp_sid'); + }); + + it('should send the gpp param if gppConsent is true in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: {gppString: 'test-consent-string', applicableSections: [7]}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gpp', 'test-consent-string'); + expect(request.data.params.gpp_sid[0]).to.be.equal(7); + }); + it('should have schain param if it is available in the bidRequest', () => { const schain = { ver: '1.0', diff --git a/test/spec/modules/minutemediaplusBidAdapter_spec.js b/test/spec/modules/minutemediaplusBidAdapter_spec.js index 33ec194c61f..5101f015b0e 100644 --- a/test/spec/modules/minutemediaplusBidAdapter_spec.js +++ b/test/spec/modules/minutemediaplusBidAdapter_spec.js @@ -423,6 +423,25 @@ describe('MinuteMediaPlus Bid Adapter', function () { 'type': 'image' }]); }) + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.minutemedia-prebid.com/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); }); describe('interpret response', function () { diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index f61987298e8..ab1fbdcc074 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -1,23 +1,70 @@ import { expect } from 'chai'; -import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec, storage } from 'modules/missenaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +const REFERRER = 'https://referer'; +const REFERRER2 = 'https://referer2'; +const COOKIE_DEPRECATION_LABEL = 'test'; describe('Missena Adapter', function () { - const adapter = newBidder(spec); + $$PREBID_GLOBAL$$.bidderSettings = { + missena: { + storageAllowed: true, + }, + }; const bidId = 'abc'; - const bid = { bidder: 'missena', bidId: bidId, sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, + ortb2: { + device: { + ext: { cdep: COOKIE_DEPRECATION_LABEL }, + }, + }, params: { apiKey: 'PA-34745704', placement: 'sticky', formats: ['sticky-banner'], }, + getFloor: (inputParams) => { + if (inputParams.mediaType === BANNER) { + return { + currency: 'EUR', + floor: 3.5, + }; + } else { + return {}; + } + }, }; + const bidWithoutFloor = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, + params: { + apiKey: 'PA-34745704', + placement: 'sticky', + formats: ['sticky-banner'], + }, + }; + const consentString = 'AAAAAAAAA=='; + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + topmostLocation: REFERRER, + canonicalUrl: 'https://canonical', + }, + }; + + const bids = [bid, bidWithoutFloor]; describe('codes', function () { it('should return a bidder code of missena', function () { expect(spec.code).to.equal('missena'); @@ -31,34 +78,27 @@ describe('Missena Adapter', function () { it('should return false if the apiKey is missing', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: {} })) + spec.isBidRequestValid(Object.assign(bid, { params: {} })), ).to.equal(false); }); it('should return false if the apiKey is an empty string', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })), ).to.equal(false); }); }); describe('buildRequests', function () { - const consentString = 'AAAAAAAAA=='; - - const bidderRequest = { - gdprConsent: { - consentString: consentString, - gdprApplies: true, - }, - refererInfo: { - topmostLocation: 'https://referer', - canonicalUrl: 'https://canonical', - }, - }; + let getDataFromLocalStorageStub = sinon.stub( + storage, + 'getDataFromLocalStorage', + ); - const requests = spec.buildRequests([bid, bid], bidderRequest); + const requests = spec.buildRequests(bids, bidderRequest); const request = requests[0]; const payload = JSON.parse(request.data); + const payloadNoFloor = JSON.parse(requests[1].data); it('should return as many server requests as bidder requests', function () { expect(requests.length).to.equal(2); @@ -81,7 +121,7 @@ describe('Missena Adapter', function () { }); it('should send referer information to the request', function () { - expect(payload.referer).to.equal('https://referer'); + expect(payload.referer).to.equal(REFERRER); expect(payload.referer_canonical).to.equal('https://canonical'); }); @@ -89,6 +129,78 @@ describe('Missena Adapter', function () { expect(payload.consent_string).to.equal(consentString); expect(payload.consent_required).to.equal(true); }); + it('should send floor data', function () { + expect(payload.floor).to.equal(3.5); + expect(payload.floor_currency).to.equal('EUR'); + }); + it('should not send floor data if not available', function () { + expect(payloadNoFloor.floor).to.equal(undefined); + expect(payloadNoFloor.floor_currency).to.equal(undefined); + }); + it('should send the idempotency key', function () { + expect(window.msna_ik).to.not.equal(undefined); + expect(payload.ik).to.equal(window.msna_ik); + }); + + getDataFromLocalStorageStub.restore(); + getDataFromLocalStorageStub = sinon.stub( + storage, + 'getDataFromLocalStorage', + ); + const localStorageData = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + }), + }; + getDataFromLocalStorageStub.callsFake((key) => localStorageData[key]); + const cappedRequests = spec.buildRequests(bids, bidderRequest); + + it('should not participate if capped', function () { + expect(cappedRequests.length).to.equal(0); + }); + + const localStorageDataSamePage = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + referer: REFERRER, + }), + }; + + getDataFromLocalStorageStub.callsFake( + (key) => localStorageDataSamePage[key], + ); + const cappedRequestsSamePage = spec.buildRequests(bids, bidderRequest); + + it('should not participate if capped on same page', function () { + expect(cappedRequestsSamePage.length).to.equal(0); + }); + + const localStorageDataOtherPage = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + referer: REFERRER2, + }), + }; + + getDataFromLocalStorageStub.callsFake( + (key) => localStorageDataOtherPage[key], + ); + const cappedRequestsOtherPage = spec.buildRequests(bids, bidderRequest); + + it('should participate if capped on a different page', function () { + expect(cappedRequestsOtherPage.length).to.equal(2); + }); + + it('should send the prebid version', function () { + expect(payload.version).to.equal('$prebid.version$'); + }); + + it('should send cookie deprecation', function () { + expect(payload.cdep).to.equal(COOKIE_DEPRECATION_LABEL); + }); }); describe('interpretResponse', function () { @@ -121,14 +233,14 @@ describe('Missena Adapter', function () { expect(result.length).to.equal(1); expect(Object.keys(result[0])).to.have.members( - Object.keys(serverResponse) + Object.keys(serverResponse), ); }); it('should return an empty response when the server answers with a timeout', function () { const result = spec.interpretResponse( { body: serverTimeoutResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); @@ -136,7 +248,7 @@ describe('Missena Adapter', function () { it('should return an empty response when the server answers with an empty ad', function () { const result = spec.interpretResponse( { body: serverEmptyAdResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js index 766f8d1a848..a4e58afbd1b 100644 --- a/test/spec/modules/mobfoxpbBidAdapter_spec.js +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -20,7 +20,8 @@ describe('MobfoxHBBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -143,6 +144,36 @@ describe('MobfoxHBBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index eaf8fa33a66..c11113473ce 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -1,16 +1,15 @@ import {expect} from 'chai'; import { - validateMultibid, - adjustBidderRequestsHook, addBidResponseHook, + adjustBidderRequestsHook, resetMultibidUnits, + resetMultiConfig, sortByMultibid, targetBidPoolHook, - resetMultiConfig + validateMultibid } from 'modules/multibid/index.js'; -import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; +import {getHighestCpm} from '../../../src/utils/reducers.js'; describe('multibid adapter', function () { let bidArray = [{ @@ -545,7 +544,7 @@ describe('multibid adapter', function () { it('it does not run filter on bidsReceived if no multibid configuration found', function () { let bids = [{...bidArray[0]}, {...bidArray[1]}]; - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -562,7 +561,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); bids.pop(); expect(result).to.not.equal(null); @@ -584,7 +583,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -609,7 +608,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -642,7 +641,7 @@ describe('multibid adapter', function () { config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm, 3); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -670,7 +669,7 @@ describe('multibid adapter', function () { expect(bidPool.length).to.equal(6); - targetBidPoolHook(callbackFn, bidPool, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bidPool, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); diff --git a/test/spec/modules/mygaruIdSystem_spec.js b/test/spec/modules/mygaruIdSystem_spec.js new file mode 100644 index 00000000000..2bfb5fdd4af --- /dev/null +++ b/test/spec/modules/mygaruIdSystem_spec.js @@ -0,0 +1,62 @@ +import { mygaruIdSubmodule } from 'modules/mygaruIdSystem.js'; +import { server } from '../../mocks/xhr'; + +describe('MygaruID module', function () { + it('should respond with async callback and get valid id', async () => { + const callBackSpy = sinon.spy(); + const expectedUrl = `https://ident.mygaru.com/v2/id?gdprApplies=0`; + const result = mygaruIdSubmodule.getId({}); + + expect(result.callback).to.be.an('function'); + const promise = result.callback(callBackSpy); + + const request = server.requests[0]; + expect(request.url).to.be.eq(expectedUrl); + + request.respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ iuid: '123' }) + ); + await promise; + + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledWith({mygaruId: '123'})).to.be.true; + }); + it('should not fail on error', async () => { + const callBackSpy = sinon.spy(); + const expectedUrl = `https://ident.mygaru.com/v2/id?gdprApplies=0`; + const result = mygaruIdSubmodule.getId({}); + + expect(result.callback).to.be.an('function'); + const promise = result.callback(callBackSpy); + + const request = server.requests[0]; + expect(request.url).to.be.eq(expectedUrl); + + request.respond( + 500, + {}, + '' + ); + await promise; + + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledWith({mygaruId: undefined})).to.be.true; + }); + + it('should not modify while decoding', () => { + const id = '222'; + const newId = mygaruIdSubmodule.decode(id) + + expect(id).to.eq(newId); + }) + it('should buildUrl with consent data', () => { + const result = mygaruIdSubmodule.getId({}, { + gdprApplies: true, + consentString: 'consentString' + }); + + expect(result.url).to.eq('https://ident.mygaru.com/v2/id?gdprApplies=1&gdprConsentString=consentString'); + }) +}); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 51e78d1f6d6..75fb357b196 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -112,7 +112,7 @@ describe('nativoBidAdapterTests', function () { bidRequests = [JSON.parse(bidRequestString)] }) - it('url should contain query string parameters', function () { + it('Request should be POST, with JSON string payload and QS params should be added to the url', function () { const request = spec.buildRequests(bidRequests, { bidderRequestId: 123456, refererInfo: { @@ -120,6 +120,11 @@ describe('nativoBidAdapterTests', function () { }, }) + expect(request.method).to.equal('POST') + + expect(request.data).to.exist + expect(request.data).to.be.a('string') + expect(request.url).to.exist expect(request.url).to.be.a('string') diff --git a/test/spec/modules/newspassidBidAdapter_spec.js b/test/spec/modules/newspassidBidAdapter_spec.js index bec6eea7bf2..6468d4f530a 100644 --- a/test/spec/modules/newspassidBidAdapter_spec.js +++ b/test/spec/modules/newspassidBidAdapter_spec.js @@ -1667,6 +1667,12 @@ describe('newspassid Adapter', function () { expect(result[0]['price']).to.equal(0.9); expect(result[0]['adserverTargeting']['np_npappnexus_adId']).to.equal('2899ec066a91ff8-0-np-1'); }); + it('should add np_auc_id (response id value)', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'np_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); + }); it('should correctly process an auction with 2 adunits & multiple bidders one of which bids for both adslots', function() { let validres = JSON.parse(JSON.stringify(multiResponse1)); let request = spec.buildRequests(multiRequest1, multiBidderRequest1.bidderRequest); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 564788c8b56..ff58671b17b 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -1,32 +1,582 @@ import { expect } from 'chai'; -import { spec } from 'modules/nextMillenniumBidAdapter.js'; +import { + getImp, + replaceUsersyncMacros, + setConsentStrings, + setOrtb2Parameters, + setEids, + spec, +} from 'modules/nextMillenniumBidAdapter.js'; + +describe('nextMillenniumBidAdapterTests', () => { + describe('function getImp', () => { + const dataTests = [ + { + title: 'imp - banner', + data: { + id: '123', + bid: { + mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, + adUnitCode: 'test-banner-1', + }, + + mediaTypes: { + banner: { + data: {sizes: [[300, 250], [320, 250]]}, + bidfloorcur: 'EUR', + bidfloor: 1.11, + }, + }, + }, -describe('nextMillenniumBidAdapterTests', function() { - const bidRequestData = [ - { - adUnitCode: 'test-div', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { placement_id: '-1' }, - sizes: [[300, 250]], - uspConsent: '1---', - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true + expected: { + id: 'test-banner-1', + bidfloorcur: 'EUR', + bidfloor: 1.11, + ext: {prebid: {storedrequest: {id: '123'}}}, + banner: {w: 300, h: 250, format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + }, }, - ortb2: { - device: { - w: 1500, - h: 1000 + + { + title: 'imp - video', + data: { + id: '234', + bid: { + mediaTypes: {video: {playerSize: [400, 300], api: [2], placement: 1, plcmt: 1}}, + adUnitCode: 'test-video-1', + }, + + mediaTypes: { + video: { + data: {playerSize: [400, 300], api: [2], placement: 1, plcmt: 1}, + bidfloorcur: 'USD', + }, + }, }, - site: { - domain: 'example.com', - page: 'http://example.com' - } + + expected: { + id: 'test-video-1', + bidfloorcur: 'USD', + ext: {prebid: {storedrequest: {id: '234'}}}, + video: { + mimes: ['video/mp4', 'video/x-ms-wmv', 'application/javascript'], + api: [2], + placement: 1, + plcmt: 1, + w: 400, + h: 300, + }, + }, + }, + + { + title: 'imp - mediaTypes.video is empty', + data: { + id: '234', + bid: { + mediaTypes: {video: {w: 640, h: 480}}, + adUnitCode: 'test-video-2', + }, + + mediaTypes: { + video: { + data: {w: 640, h: 480}, + bidfloorcur: 'USD', + }, + }, + }, + + expected: { + id: 'test-video-2', + bidfloorcur: 'USD', + ext: {prebid: {storedrequest: {id: '234'}}}, + video: {w: 640, h: 480, mimes: ['video/mp4', 'video/x-ms-wmv', 'application/javascript']}, + }, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {bid, id, mediaTypes} = data; + const imp = getImp(bid, id, mediaTypes); + expect(imp).to.deep.equal(expected); + }); + } + }); + + describe('function setConsentStrings', () => { + const dataTests = [ + { + title: 'full: uspConsent, gdprConsent and gppConsent', + data: { + postBody: {}, + bidderRequest: { + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + }, + }, + + expected: { + user: {ext: {consent: 'kjfdniwjnifwenrif3'}}, + regs: { + gpp: 'DBACNYA~CPXxRfAPXxR', + gpp_sid: [7], + ext: {gdpr: 1, us_privacy: '1---'}, + }, + }, + }, + + { + title: 'gdprConsent(false) and ortb2(gpp)', + data: { + postBody: {}, + bidderRequest: { + gdprConsent: {consentString: 'ewtewbefbawyadexv', gdprApplies: false}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + }, + }, + + expected: { + user: {ext: {consent: 'ewtewbefbawyadexv'}}, + regs: { + gpp: 'DSFHFHWEUYVDC', + gpp_sid: [8, 9, 10], + ext: {gdpr: 0}, + }, + }, + }, + + { + title: 'gdprConsent(false)', + data: { + postBody: {}, + bidderRequest: {gdprConsent: {gdprApplies: false}}, + }, + + expected: { + regs: {ext: {gdpr: 0}}, + }, + }, + + { + title: 'empty', + data: { + postBody: {}, + bidderRequest: {}, + }, + + expected: {}, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {postBody, bidderRequest} = data; + setConsentStrings(postBody, bidderRequest); + expect(postBody).to.deep.equal(expected); + }); + } + }); + + describe('function replaceUsersyncMacros', () => { + const dataTests = [ + { + title: 'url with all macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + type: 'image', + }, + + expected: 'https://some.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=image', + }, + + { + title: 'url with some macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&type={{.TYPE_PIXEL}}', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: false}, + type: 'iframe', + }, + + expected: 'https://some.url?gdpr=0&gdpr_consent=kjfdniwjnifwenrif3&type=iframe', + }, + + { + title: 'url without macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?param1=value1¶m2=value2', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: false}, + type: 'iframe', + }, + + expected: 'https://some.url?param1=value1¶m2=value2', + }, + + { + title: 'url with all macroses - consents are empty', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}', + }, + + expected: 'https://some.url?gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid=&type=', + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {url, gdprConsent, uspConsent, gppConsent, type} = data; + const newUrl = replaceUsersyncMacros(url, gdprConsent, uspConsent, gppConsent, type); + expect(newUrl).to.equal(expected); + }); + } + }); + + describe('function spec.getUserSyncs', () => { + const dataTests = [ + { + title: 'pixels from responses ({iframeEnabled: true, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: true}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + + {body: {ext: {sync: { + iframe: [ + 'https://some.6.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.7.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + + {body: {ext: {sync: { + image: [ + 'https://some.8.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://some.1.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.2.url?us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.3.url?param=1234'}, + {type: 'iframe', url: 'https://some.4.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.5.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + {type: 'iframe', url: 'https://some.6.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.7.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + {type: 'image', url: 'https://some.8.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + ], + }, + + { + title: 'pixels from responses ({iframeEnabled: true, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: false}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'iframe', url: 'https://some.4.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.5.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + ], + }, + + { + title: 'pixels from responses ({iframeEnabled: false, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: false, pixelEnabled: true}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://some.1.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.2.url?us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.3.url?param=1234'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: true, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: true}, + responses: [], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=image'}, + {type: 'iframe', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=iframe'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: true, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: false}, + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'iframe', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=iframe'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: false, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: false, pixelEnabled: false}, + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [], + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {syncOptions, responses, gdprConsent, uspConsent, gppConsent} = data; + const pixels = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent); + expect(pixels).to.deep.equal(expected); + }); + } + }); + + describe('function setOrtb2Parameters', () => { + const dataTests = [ + { + title: 'site.pagecat, site.content.cat and site.content.language', + data: { + postBody: {}, + ortb2: {site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + }}, + }, + + expected: {site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + }}, + }, + + { + title: 'site.keywords, site.content.keywords and user.keywords', + data: { + postBody: {}, + ortb2: { + user: {keywords: 'key7,key8,key9'}, + site: { + keywords: 'key1,key2,key3', + content: {keywords: 'key4,key5,key6'}, + }, + }, + }, + + expected: { + user: {keywords: 'key7,key8,key9'}, + site: { + keywords: 'key1,key2,key3', + content: {keywords: 'key4,key5,key6'}, + }, + }, + }, + + { + title: 'only site.content.language', + data: { + postBody: {site: {domain: 'some.domain'}}, + ortb2: {site: { + content: {language: 'EN'}, + }}, + }, + + expected: {site: { + domain: 'some.domain', + content: {language: 'EN'}, + }}, + }, + + { + title: 'object ortb2 is empty', + data: { + postBody: {imp: []}, + }, + + expected: {imp: []}, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {postBody, ortb2} = data; + setOrtb2Parameters(postBody, ortb2); + expect(postBody).to.deep.equal(expected); + }); + }; + }); + + describe('function setEids', () => { + const dataTests = [ + { + title: 'setEids - userIdAsEids is empty', + data: { + postBody: {}, + bid: { + userIdAsEids: undefined, + }, + }, + + expected: {}, + }, + + { + title: 'setEids - userIdAsEids - array is empty', + data: { + postBody: {}, + bid: { + userIdAsEids: [], + }, + }, + + expected: {}, + }, + + { + title: 'setEids - userIdAsEids is', + data: { + postBody: {}, + bid: { + userIdAsEids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + }, + + expected: { + user: { + eids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + }, + }, + ]; + + for (let { title, data, expected } of dataTests) { + it(title, () => { + const { postBody, bid } = data; + setEids(postBody, bid); + expect(postBody).to.deep.equal(expected); + }); + } + }); + + const bidRequestData = [{ + adUnitCode: 'test-div', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { placement_id: '-1' }, + sizes: [[300, 250]], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + }, + + ortb2: { + device: { + w: 1500, + h: 1000 + }, + + site: { + domain: 'example.com', + page: 'http://example.com' } } - ]; + }]; const serverResponse = { body: { @@ -49,7 +599,7 @@ describe('nextMillenniumBidAdapterTests', function() { cur: 'USD', ext: { sync: { - image: ['urlA?gdpr={{.GDPR}}'], + image: ['urlA?gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}'], iframe: ['urlB'], } } @@ -117,61 +667,6 @@ describe('nextMillenniumBidAdapterTests', function() { }, ]; - it('Request params check with GDPR and USP Consent', function () { - const request = spec.buildRequests(bidRequestData, bidRequestData[0]); - expect(JSON.parse(request[0].data).user.ext.consent).to.equal('kjfdniwjnifwenrif3'); - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); - }); - - it('Test getUserSyncs function', function () { - const syncOptions = { - 'iframeEnabled': false, - 'pixelEnabled': true - } - let userSync = spec.getUserSyncs(syncOptions, [serverResponse], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.equal('image'); - expect(userSync[0].url).to.equal('urlA?gdpr=1'); - - syncOptions.iframeEnabled = true; - syncOptions.pixelEnabled = false; - userSync = spec.getUserSyncs(syncOptions, [serverResponse], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.equal('iframe'); - expect(userSync[0].url).to.equal('urlB'); - }); - - it('Test getUserSyncs with no response', function () { - const syncOptions = { - 'iframeEnabled': true, - 'pixelEnabled': false - } - let userSync = spec.getUserSyncs(syncOptions, [], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array') - expect(userSync[0].type).to.equal('iframe') - expect(userSync[0].url).to.equal('https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&type=iframe') - }) - - it('Test getUserSyncs function if GDPR is undefined', function () { - const syncOptions = { - 'iframeEnabled': false, - 'pixelEnabled': true - } - - let userSync = spec.getUserSyncs(syncOptions, [serverResponse], undefined, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.equal('image'); - expect(userSync[0].url).to.equal('urlA?gdpr=0'); - }); - - it('Request params check without GDPR Consent', function () { - delete bidRequestData[0].gdprConsent - const request = spec.buildRequests(bidRequestData, bidRequestData[0]); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); - }); - it('validate_generated_params', function() { const request = spec.buildRequests(bidRequestData, {bidderRequestId: 'mock-uuid'}); expect(request[0].bidId).to.equal('bid1234'); @@ -190,7 +685,7 @@ describe('nextMillenniumBidAdapterTests', function() { it('Check if refresh_count param is incremented', function() { const request = spec.buildRequests(bidRequestData); - expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); + expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(1); }); it('Check if domain was added', function() { @@ -439,7 +934,7 @@ describe('nextMillenniumBidAdapterTests', function() { ]; for (let {eventName, bid, expected} of dataForTests) { - const url = spec.getUrlPixelMetric(eventName, bid); + const url = spec._getUrlPixelMetric(eventName, bid); expect(url).to.equal(expected); }; }) diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7c2cea99a46..f18e0365226 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -20,12 +20,12 @@ const instreamResponse = { 'crid': '97517771', 'h': 1, 'w': 1, + 'adm': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ', 'ext': { 'mediaType': 'instream', 'ssp': 'appnexus', 'divId': 'video1', 'adUnitCode': 'video1', - 'vastXml': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' } } ], @@ -374,7 +374,7 @@ describe('Nexx360 bid adapter tests', function () { expect(requestContent.imp[1].tagid).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.adUnitCode).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.divId).to.be.eql('div-2-abcd'); - expect(requestContent.ext.bidderVersion).to.be.eql('2.0'); + expect(requestContent.ext.bidderVersion).to.be.eql('4.0'); expect(requestContent.ext.source).to.be.eql('prebid.js'); }); @@ -434,7 +434,7 @@ describe('Nexx360 bid adapter tests', function () { const output = spec.interpretResponse(response); expect(output.length).to.be.eql(0); }); - it('banner responses', function() { + it('banner responses with adUrl only', function() { const response = { body: { 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', @@ -479,6 +479,53 @@ describe('Nexx360 bid adapter tests', function () { expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); }); + it('banner responses with adm', function() { + const response = { + body: { + 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'id': '4427551302944024629', + 'impid': '226175918ebeda', + 'price': 1.5, + 'adomain': [ + 'http://prebid.org' + ], + 'crid': '98493581', + 'ssp': 'appnexus', + 'h': 600, + 'w': 300, + 'adm': '
TestAd
', + 'cat': [ + 'IAB3-1' + ], + 'ext': { + 'adUnitCode': 'div-1', + 'mediaType': 'banner', + 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + 'ssp': 'appnexus', + } + } + ], + 'seat': 'appnexus' + } + ], + 'ext': { + 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + 'cookies': [] + }, + } + }; + const output = spec.interpretResponse(response); + expect(output[0].ad).to.be.eql(response.body.seatbid[0].bid[0].adm); + expect(output[0].adUrl).to.be.eql(undefined); + expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); + expect(output[0].currency).to.be.eql(response.body.cur); + expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + }); it('instream responses', function() { const response = { body: { @@ -514,7 +561,7 @@ describe('Nexx360 bid adapter tests', function () { } }; const output = spec.interpretResponse(response); - expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].ext.vastXml); + expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].adm); expect(output[0].mediaType).to.be.eql('video'); expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); @@ -538,11 +585,11 @@ describe('Nexx360 bid adapter tests', function () { 'crid': '97517771', 'h': 1, 'w': 1, + 'adm': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ', 'ext': { 'mediaType': 'outstream', 'ssp': 'appnexus', 'adUnitCode': 'div-1', - 'vastXml': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' } } ], @@ -555,7 +602,7 @@ describe('Nexx360 bid adapter tests', function () { } }; const output = spec.interpretResponse(response); - expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].ext.vastXml); + expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].adm); expect(output[0].mediaType).to.be.eql('video'); expect(output[0].currency).to.be.eql(response.body.cur); expect(typeof output[0].renderer).to.be.eql('object'); diff --git a/test/spec/modules/nobidAnalyticsAdapter_spec.js b/test/spec/modules/nobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..f6c741bb7ff --- /dev/null +++ b/test/spec/modules/nobidAnalyticsAdapter_spec.js @@ -0,0 +1,616 @@ +import nobidAnalytics from 'modules/nobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +let events = require('src/events'); +let adapterManager = require('src/adapterManager').default; +let constants = require('src/constants.json'); + +const TOP_LOCATION = 'https://www.somesite.com'; +const SITE_ID = 1234; + +describe('NoBid Prebid Analytic', function () { + var clock; + describe('enableAnalytics', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('auctionInit test', function (done) { + const initOptions = { + options: { + /* siteId: SITE_ID */ + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions).to.equal(undefined); + + initOptions.options.siteId = SITE_ID; + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions.siteId).to.equal(SITE_ID); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + expect(nobidAnalytics.initOptions).to.have.property('siteId', SITE_ID); + expect(nobidAnalytics).to.have.property('topLocation', TOP_LOCATION); + + const data = { ts: Date.now() }; + clock.tick(5000); + const expired = nobidAnalytics.isExpired(data); + expect(expired).to.equal(false); + + done(); + }); + + it('BID_REQUESTED/BID_RESPONSE/BID_TIMEOUT/AD_RENDER_SUCCEEDED test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + events.emit(constants.EVENTS.BID_WON, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_REQUESTED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_RESPONSE, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + done(); + }); + + it('bidWon test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + bidderCode: 'nobid', + width: 728, + height: 9, + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + transactionId: 'd58cbeae-92c8-4262-ba8d-0e649cbf5470', + auctionId: 'd758cce5-d178-408c-b777-8cac605ef7ca', + mediaType: 'banner', + source: 'client', + cpm: 6.4, + currency: 'EUR', + creativeId: 'TEST', + dealId: '', + netRevenue: true, + ttl: 300, + ad: 'AD HERE', + meta: { + advertiserDomains: ['advertiser_domain.com'] + }, + metrics: { + 'requestBids.usp': 0 + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692156287517, + requestTimestamp: 1692156286972, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 545, + pbCg: '', + size: '728x90', + adserverTargeting: { + hb_bidder: 'nobid', + hb_adid: '106d14b7d06b607', + hb_pb: '6.40', + hb_size: '728x90', + hb_source: 'client', + hb_format: 'banner', + hb_adomain: 'advertiser_domain.com', + 'hb_crid': 'TEST' + }, + status: 'rendered', + params: [ + { + siteId: SITE_ID + } + ] + }; + + const expectedOutgoingRequest = { + version: nobidAnalyticsVersion, + bidderCode: 'nobid', + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + mediaType: 'banner', + cpm: 6.4, + currency: 'EUR', + originalCpm: 6.44, + originalCurrency: 'USD', + adUnitCode: 'leaderboard', + timeToRespond: 545, + size: '728x90', + topLocation: TOP_LOCATION + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.BID_WON, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const bidWonRequest = JSON.parse(server.requests[0].requestBody); + expect(bidWonRequest).to.have.property('version', nobidAnalyticsVersion); + expect(bidWonRequest).to.have.property('bidderCode', expectedOutgoingRequest.bidderCode); + expect(bidWonRequest).to.have.property('statusMessage', expectedOutgoingRequest.statusMessage); + expect(bidWonRequest).to.have.property('adId', expectedOutgoingRequest.adId); + expect(bidWonRequest).to.have.property('requestId', expectedOutgoingRequest.requestId); + expect(bidWonRequest).to.have.property('mediaType', expectedOutgoingRequest.mediaType); + expect(bidWonRequest).to.have.property('cpm', expectedOutgoingRequest.cpm); + expect(bidWonRequest).to.have.property('currency', expectedOutgoingRequest.currency); + expect(bidWonRequest).to.have.property('originalCpm', expectedOutgoingRequest.originalCpm); + expect(bidWonRequest).to.have.property('originalCurrency', expectedOutgoingRequest.originalCurrency); + expect(bidWonRequest).to.have.property('adUnitCode', expectedOutgoingRequest.adUnitCode); + expect(bidWonRequest).to.have.property('timeToRespond', expectedOutgoingRequest.timeToRespond); + expect(bidWonRequest).to.have.property('size', expectedOutgoingRequest.size); + expect(bidWonRequest).to.have.property('topLocation', expectedOutgoingRequest.topLocation); + expect(bidWonRequest).to.not.have.property('pbCg'); + + done(); + }); + + it('auctionEnd test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + timestamp: 1692224437573, + auctionEnd: 1692224437986, + auctionStatus: 'completed', + adUnits: [ + { + code: 'leaderboard', + sizes: [[728, 90]], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[300, 250]] }, + { minViewPort: [750, 0], sizes: [[728, 90]] } + ], + adunit: '/111111/adunit', + bids: [{ bidder: 'nobid', params: { siteId: SITE_ID } }] + } + ], + adUnitCodes: ['leaderboard'], + bidderRequests: [ + { + bidderCode: 'nobid', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequestId: '5beedb9f99ad98', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + sizes: [[728, 90]], + bidId: '6ef0277f36c8df', + bidderRequestId: '5beedb9f99ad98', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2: { + site: { + domain: 'site.me', + publisher: { + domain: 'site.me' + }, + page: TOP_LOCATION + }, + device: { + w: 2605, + h: 895, + dnt: 0, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + language: 'en', + } + } + } + ], + auctionStart: 1692224437573, + timeout: 3000, + refererInfo: { + topmostLocation: TOP_LOCATION, + location: TOP_LOCATION, + page: TOP_LOCATION, + domain: 'site.me', + ref: null, + } + } + ], + noBids: [ + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + statusMessage: 'Bid available', + adId: '95781b6ae5ef2f', + requestId: '6ef0277f36c8df', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + mediaType: 'banner', + source: 'client', + cpm: 5.93, + currency: 'EUR', + creativeId: 'TEST', + dealId: '', + netRevenue: true, + ttl: 300, + ad: '', + meta: { + advertiserDomains: [ + 'advertiser_domain.com' + ] + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692224437982, + requestTimestamp: 1692224437576, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 0, + pbLg: 5.00, + pbCg: '', + size: '728x90', + adserverTargeting: { hb_bidder: 'nobid', hb_pb: '6.40' }, + status: 'targetingSet' + } + ], + bidsRejected: [], + winningBids: [], + timeout: 3000 + }; + + const expectedOutgoingRequest = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequests: [ + { + bidderCode: 'nobid', + bidderRequestId: '7c1940bb285731', + bids: [ + { + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + sizes: [[728, 90]], + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1 + } + ], + refererInfo: { + topmostLocation: TOP_LOCATION + } + } + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 5.93, + currency: 'EUR', + originalCpm: 6.44, + originalCurrency: 'USD', + adUnitCode: 'leaderboard' + } + ] + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: `${TOP_LOCATION}_something`}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.AUCTION_END, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const auctionEndRequest = JSON.parse(server.requests[0].requestBody); + expect(auctionEndRequest).to.have.property('version', nobidAnalyticsVersion); + expect(auctionEndRequest).to.have.property('auctionId', expectedOutgoingRequest.auctionId); + expect(auctionEndRequest.bidderRequests).to.have.length(1); + expect(auctionEndRequest.bidderRequests[0].bidderCode).to.equal(expectedOutgoingRequest.bidderRequests[0].bidderCode); + expect(auctionEndRequest.bidderRequests[0].bids).to.have.length(1); + expect(typeof auctionEndRequest.bidderRequests[0].bids[0].bidder).to.equal('undefined'); + expect(auctionEndRequest.bidderRequests[0].bids[0].adUnitCode).to.equal(expectedOutgoingRequest.bidderRequests[0].bids[0].adUnitCode); + expect(typeof auctionEndRequest.bidderRequests[0].bids[0].params).to.equal('undefined'); + expect(typeof auctionEndRequest.bidderRequests[0].bids[0].src).to.equal('undefined'); + expect(auctionEndRequest.bidderRequests[0].refererInfo.topmostLocation).to.equal(expectedOutgoingRequest.bidderRequests[0].refererInfo.topmostLocation); + expect(auctionEndRequest.bidsReceived).to.have.length(1); + expect(auctionEndRequest.bidsReceived[0].bidderCode).to.equal(expectedOutgoingRequest.bidsReceived[0].bidderCode); + expect(auctionEndRequest.bidsReceived[0].width).to.equal(expectedOutgoingRequest.bidsReceived[0].width); + expect(auctionEndRequest.bidsReceived[0].height).to.equal(expectedOutgoingRequest.bidsReceived[0].height); + expect(auctionEndRequest.bidsReceived[0].mediaType).to.equal(expectedOutgoingRequest.bidsReceived[0].mediaType); + expect(auctionEndRequest.bidsReceived[0].cpm).to.equal(expectedOutgoingRequest.bidsReceived[0].cpm); + expect(auctionEndRequest.bidsReceived[0].currency).to.equal(expectedOutgoingRequest.bidsReceived[0].currency); + expect(auctionEndRequest.bidsReceived[0].originalCpm).to.equal(expectedOutgoingRequest.bidsReceived[0].originalCpm); + expect(auctionEndRequest.bidsReceived[0].originalCurrency).to.equal(expectedOutgoingRequest.bidsReceived[0].originalCurrency); + expect(auctionEndRequest.bidsReceived[0].adUnitCode).to.equal(expectedOutgoingRequest.bidsReceived[0].adUnitCode); + expect(typeof auctionEndRequest.bidsReceived[0].source).to.equal('undefined'); + + done(); + }); + + it('Analytics disabled test', function (done) { + let disabled; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 0})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678901'}); + clock.tick(1000); + expect(server.requests).to.have.length(2); + + nobidAnalytics.processServerResponse('disabled: true'); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(1000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 1})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(5000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.retentionSeconds = 5; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 1})); + clock.tick(1000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + clock.tick(6000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + + done(); + }); + }); + + describe('Analytics disabled event type test', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('Analytics disabled event type test', function (done) { + // Initialize adapter + const initOptions = { options: { siteId: SITE_ID } }; + nobidAnalytics.enableAnalytics(initOptions); + adapterManager.enableAnalytics({ provider: 'nobid', options: initOptions }); + + let eventType = constants.EVENTS.AUCTION_END; + let disabled; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 0})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + events.emit(eventType, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + events.emit(eventType, {auctionId: '12345678901'}); + clock.tick(1000); + expect(server.requests).to.have.length(2); + + server.requests.length = 0; + expect(server.requests).to.have.length(0); + + nobidAnalytics.processServerResponse(JSON.stringify({disabled_auctionEnd: 1})); + disabled = nobidAnalytics.isAnalyticsDisabled(eventType); + expect(disabled).to.equal(true); + events.emit(eventType, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(0); + + server.requests.length = 0; + + nobidAnalytics.processServerResponse(JSON.stringify({disabled_auctionEnd: 0})); + disabled = nobidAnalytics.isAnalyticsDisabled(eventType); + expect(disabled).to.equal(false); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + + server.requests.length = 0; + expect(server.requests).to.have.length(0); + + eventType = constants.EVENTS.BID_WON; + nobidAnalytics.processServerResponse(JSON.stringify({disabled_bidWon: 1})); + disabled = nobidAnalytics.isAnalyticsDisabled(eventType); + expect(disabled).to.equal(true); + events.emit(eventType, {bidderCode: 'nobid'}); + clock.tick(1000); + expect(server.requests).to.have.length(0); + + server.requests.length = 0; + expect(server.requests).to.have.length(0); + + eventType = constants.EVENTS.AUCTION_END; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 1})); + disabled = nobidAnalytics.isAnalyticsDisabled(eventType); + expect(disabled).to.equal(true); + events.emit(eventType, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(0); + + server.requests.length = 0; + expect(server.requests).to.have.length(0); + + eventType = constants.EVENTS.AUCTION_END; + nobidAnalytics.processServerResponse(JSON.stringify({disabled_auctionEnd: 1, disabled_bidWon: 0})); + disabled = nobidAnalytics.isAnalyticsDisabled(eventType); + expect(disabled).to.equal(true); + events.emit(eventType, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(0); + disabled = nobidAnalytics.isAnalyticsDisabled(constants.EVENTS.BID_WON); + expect(disabled).to.equal(false); + events.emit(constants.EVENTS.BID_WON, {bidderCode: 'nobid'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + + done(); + }); + }); + + describe('NoBid Carbonizer', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('Carbonizer test', function (done) { + let active = nobidCarbonizer.isActive(); + expect(active).to.equal(false); + + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: false})); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(false); + + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(true); + + const previousRetention = nobidAnalytics.retentionSeconds; + nobidAnalytics.retentionSeconds = 3; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + let stored = nobidCarbonizer.getStoredLocalData(); + expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain(`{"carbonizer_active":true,"ts":`); + clock.tick(5000); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(false); + + nobidAnalytics.retentionSeconds = previousRetention; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(true); + + let adunits = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + nobidCarbonizer.carbonizeAdunits(adunits, true); + stored = nobidCarbonizer.getStoredLocalData(); + expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain('{"carbonizer_active":true,"ts":'); + expect(stored[nobidAnalytics.ANALYTICS_OPT_NAME]).to.contain('{"bidder1":1,"bidder2":1}'); + clock.tick(5000); + expect(adunits[0].bids.length).to.equal(0); + + done(); + }); + }); +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 8328aae33d8..b1e303bde6e 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -39,8 +39,6 @@ describe('Nobid Adapter', function () { it('should FLoor = 1', function () { spec.buildRequests(bidRequests, bidderRequest); const request = spec.buildRequests(bidRequests, bidderRequest); - /* eslint-disable no-console */ - console.log('request.data:', request.data); const payload = JSON.parse(request.data); expect(payload.a[0].floor).to.equal(1); }); @@ -145,6 +143,61 @@ describe('Nobid Adapter', function () { }); }); + describe('Request with GPP', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + const BIDDER_CODE = 'duration'; + let bidRequests = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'siteId': SITE_ID + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + } + ]; + + const GPP = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + const GPP_SID = [1, 3]; + + const bidderRequest = { + refererInfo: {page: REFERER}, + bidderCode: BIDDER_CODE, + gppConsent: {gppString: GPP, applicableSections: GPP_SID} + } + + it('gpp should match', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(payload.gpp).to.equal(GPP); + expect(payload.gpp_sid.join(',')).to.equal(GPP_SID.join(',')); + }); + + it('gpp should not be set', function () { + delete bidderRequest.gppConsent.applicableSections; + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(typeof payload.gpp).to.equal('undefined'); + expect(typeof payload.gpp_sid).to.equal('undefined'); + }); + + it('gpp ortb2 should match', function () { + delete bidderRequest.gppConsent; + bidderRequest.ortb2 = {regs: {gpp: GPP, gpp_sid: GPP_SID}}; + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(payload.gpp).to.equal(GPP); + expect(payload.gpp_sid.join(',')).to.equal(GPP_SID.join(',')); + }); + }); + describe('isDurationBidRequestValid', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index bbe53855094..aad753571a8 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' @@ -41,7 +42,21 @@ describe('OguryBidAdapter', function () { return floorResult; }, - transactionId: 'transactionId' + transactionId: 'transactionId', + userId: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + userIdAsEids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ] }, { adUnitCode: 'adUnitCode2', @@ -406,12 +421,26 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, + uids: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + eids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ], }, }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.5.0' + adapterversion: '1.6.0' }, device: { w: stubbedWidth, @@ -636,7 +665,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -662,7 +693,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -688,7 +721,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -700,6 +735,48 @@ describe('OguryBidAdapter', function () { expect(request.data.regs.ext.gdpr).to.be.a('number'); }); + it('should should not add uids infos if userId is undefined', () => { + const expectedRequestWithUndefinedUserId = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + eids: expectedRequestObject.user.ext.eids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userId: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserId); + }); + + it('should should not add uids infos if userIdAsEids is undefined', () => { + const expectedRequestWithUndefinedUserIdAsEids = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + uids: expectedRequestObject.user.ext.uids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userIdAsEids: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserIdAsEids); + }); + it('should handle bidFloor undefined', () => { const expectedRequestWithUndefinedFloor = { ...expectedRequestObject @@ -813,7 +890,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.5.0', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -830,7 +907,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.5.0', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }] @@ -851,20 +928,11 @@ describe('OguryBidAdapter', function () { }); describe('onBidWon', function() { - const nurl = 'https://fakewinurl.test'; - let xhr; + const nurl = 'https://fakewinurl.test/'; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - }) - - afterEach(function() { - xhr.restore() + requests = server.requests; }) it('Should not create nurl request if bid is undefined', function() { @@ -932,21 +1000,15 @@ describe('OguryBidAdapter', function () { }) describe('onTimeout', function () { - let xhr; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { + requests = server.requests; + server.onCreate = (xhr) => { requests.push(xhr); }; }) - afterEach(function() { - xhr.restore() - }) - it('should send on bid timeout notification', function() { const bid = { ad: 'cookies', diff --git a/test/spec/modules/omsBidAdapter_spec.js b/test/spec/modules/omsBidAdapter_spec.js new file mode 100644 index 00000000000..10a9c4c946c --- /dev/null +++ b/test/spec/modules/omsBidAdapter_spec.js @@ -0,0 +1,420 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import {spec} from 'modules/omsBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {config} from '../../../src/config'; + +const URL = 'https://rt.marphezis.com/hb'; + +describe('omsBidAdapter', function () { + const adapter = newBidder(spec); + let element, win; + let bidRequests; + let sandbox; + + beforeEach(function () { + element = { + x: 0, + y: 0, + + width: 0, + height: 0, + + getBoundingClientRect: () => { + return { + width: element.width, + height: element.height, + + left: element.x, + top: element.y, + right: element.x + element.width, + bottom: element.y + element.height + }; + } + }; + win = { + document: { + visibilityState: 'visible' + }, + + innerWidth: 800, + innerHeight: 600 + }; + bidRequests = [{ + 'bidder': 'oms', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + sandbox = sinon.sandbox.create(); + sandbox.stub(document, 'getElementById').withArgs('adunit-code').returns(element); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns(win); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'oms', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when publisherId not passed correctly', function () { + bid.params.publisherId = undefined; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.method).to.equal('POST'); + }); + + it('request url should match our endpoint url', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(URL); + }); + + it('sets the proper banner object', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + }); + + it('accepts a single array as a size', function () { + bidRequests[0].mediaTypes.banner.sizes = [300, 250]; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + }); + + it('sends bidfloor param if present', function () { + bidRequests[0].params.bidFloor = 0.05; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + }); + + it('sends tagid', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].tagid).to.equal('adunit-code'); + }); + + it('sends publisher id', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.site.publisher.id).to.equal(1234567); + }); + + it('sends gdpr info if exists', function () { + const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const bidderRequest = { + 'bidderCode': 'oms', + 'auctionId': '1d1a030790a437', + 'bidderRequestId': '22edbae2744bf5', + 'timeout': 3000, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + refererInfo: { + page: 'http://example.com/page.html', + domain: 'example.com', + } + }; + bidderRequest.bids = bidRequests; + + const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); + + expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.user.ext.consent).to.exist.and.to.be.a('string'); + expect(data.user.ext.consent).to.equal(consentString); + }); + + it('sends coppa', function () { + const data = JSON.parse(spec.buildRequests(bidRequests, {ortb2: {regs: {coppa: 1}}}).data) + expect(data.regs).to.not.be.undefined; + expect(data.regs.coppa).to.equal(1); + }); + + it('sends schain', function () { + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data).to.not.be.undefined; + expect(data.source).to.not.be.undefined; + expect(data.source.ext).to.not.be.undefined; + expect(data.source.ext.schain).to.not.be.undefined; + expect(data.source.ext.schain.complete).to.equal(1); + expect(data.source.ext.schain.ver).to.equal('1.0'); + expect(data.source.ext.schain.nodes).to.not.be.undefined; + expect(data.source.ext.schain.nodes).to.lengthOf(1); + expect(data.source.ext.schain.nodes[0].asi).to.equal('exchange1.com'); + expect(data.source.ext.schain.nodes[0].sid).to.equal('1234'); + expect(data.source.ext.schain.nodes[0].hp).to.equal(1); + expect(data.source.ext.schain.nodes[0].rid).to.equal('bid-request-1'); + expect(data.source.ext.schain.nodes[0].name).to.equal('publisher'); + expect(data.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); + }); + + it('sends user eid parameters', function () { + bidRequests[0].userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: 'userid_pubcid' + }] + }, { + source: 'adserver.org', + uids: [{ + id: 'userid_ttd', + ext: { + rtiPartner: 'TDID' + } + }] + } + ]; + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + + expect(data.user).to.not.be.undefined; + expect(data.user.ext).to.not.be.undefined; + expect(data.user.ext.eids).to.not.be.undefined; + expect(data.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); + }); + + it('sends user id parameters', function () { + const userId = { + sharedid: { + id: '01*******', + third: '01E*******' + } + }; + + bidRequests[0].userId = userId; + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data.user).to.not.be.undefined; + expect(data.user.ext).to.not.be.undefined; + expect(data.user.ext.ids).is.deep.equal(userId); + }); + + it('sends gpid parameters', function () { + bidRequests[0].ortb2Imp = { + 'ext': { + 'gpid': '/1111/home-left', + 'data': { + 'adserver': { + 'name': 'gam', + 'adslot': '/1111/home' + }, + 'pbadslot': '/1111/home-left' + } + } + } + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data.imp[0].ext).to.not.be.undefined; + expect(data.imp[0].ext.gpid).to.not.be.undefined; + expect(data.imp[0].ext.adserverName).to.not.be.undefined; + expect(data.imp[0].ext.adslot).to.not.be.undefined; + expect(data.imp[0].ext.pbadslot).to.not.be.undefined; + }); + + context('when element is fully in view', function () { + it('returns 100', function () { + Object.assign(element, {width: 600, height: 400}); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(100); + }); + }); + + context('when element is out of view', function () { + it('returns 0', function () { + Object.assign(element, {x: -300, y: 0, width: 207, height: 320}); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(0); + }); + }); + + context('when element is partially in view', function () { + it('returns percentage', function () { + Object.assign(element, {width: 800, height: 800}); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(75); + }); + }); + + context('when width or height of the element is zero', function () { + it('try to use alternative values', function () { + Object.assign(element, {width: 0, height: 0}); + bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(25); + }); + }); + + context('when nested iframes', function () { + it('returns \'na\'', function () { + Object.assign(element, {width: 600, height: 400}); + + utils.getWindowTop.restore(); + utils.getWindowSelf.restore(); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns({}); + + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal('na'); + }); + }); + + context('when tab is inactive', function () { + it('returns 0', function () { + Object.assign(element, {width: 600, height: 400}); + + utils.getWindowTop.restore(); + win.document.visibilityState = 'hidden'; + sandbox.stub(utils, 'getWindowTop').returns(win); + + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(0); + }); + }); + }); + + describe('interpretResponse', function () { + let response; + beforeEach(function () { + response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': '376874781', + 'impid': '283a9f4cd2415d', + 'price': 0.35743275, + 'nurl': '', + 'adm': '', + 'w': 300, + 'h': 250, + 'adomain': ['example.com'] + }] + }] + } + }; + }); + + it('should get the correct bid response', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': '376874781', + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': `
`, + 'ttl': 300, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('crid should default to the bid id if not on the response', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': response.body.seatbid[0].bid[0].id, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': `
`, + 'ttl': 300, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('handles empty bid response', function () { + let response = { + body: '' + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe('getUserSyncs ', () => { + let syncOptions = {iframeEnabled: true, pixelEnabled: true}; + + it('should not return', () => { + let returnStatement = spec.getUserSyncs(syncOptions, []); + expect(returnStatement).to.be.empty; + }); + }); +}); diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 6c9ba05bacd..93db5ffc57f 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -15,9 +15,14 @@ describe('onetag', function () { 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', - ortb2Imp: { - ext: { - tid: 'qwerty123' + 'ortb2Imp': { + 'ext': { + 'tid': '0000' + } + }, + 'ortb2': { + 'source': { + 'tid': '1111' } }, 'schain': { @@ -184,7 +189,7 @@ describe('onetag', function () { }); it('Should contain all keys', function () { expect(data).to.be.an('object'); - expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version'); + expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version', 'fledgeEnabled'); expect(data.location).to.satisfy(function (value) { return value === null || typeof value === 'string'; }); @@ -208,6 +213,7 @@ describe('onetag', function () { expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { return value === null || typeof value === 'string' }); + expect(data.fledgeEnabled).to.be.a('boolean'); expect(data.bids).to.be.an('array'); expect(data.version).to.have.all.keys('prebid', 'adapter'); const bids = data['bids']; @@ -256,8 +262,18 @@ describe('onetag', function () { expect(dataObj.bids).to.be.an('array').that.is.empty; } catch (e) { } }); + it('Should pick each bid\'s auctionId and transactionId from ortb2 related fields', function () { + const serverRequest = spec.buildRequests([bannerBid]); + const payload = JSON.parse(serverRequest.data); + + expect(payload).to.exist; + expect(payload.bids).to.exist.and.to.have.length(1); + expect(payload.bids[0].auctionId).to.equal(bannerBid.ortb2.source.tid); + expect(payload.bids[0].transactionId).to.equal(bannerBid.ortb2Imp.ext.tid); + }); it('should send GDPR consent data', function () { let consentString = 'consentString'; + let addtlConsent = '2~1.35.41.101~dv.9.21.81'; let bidderRequest = { 'bidderCode': 'onetag', 'auctionId': '1d1a030790a475', @@ -265,7 +281,8 @@ describe('onetag', function () { 'timeout': 3000, 'gdprConsent': { consentString: consentString, - gdprApplies: true + gdprApplies: true, + addtlConsent: addtlConsent } }; let serverRequest = spec.buildRequests([bannerBid], bidderRequest); @@ -274,6 +291,7 @@ describe('onetag', function () { expect(payload).to.exist; expect(payload.gdprConsent).to.exist; expect(payload.gdprConsent.consentString).to.exist.and.to.equal(consentString); + expect(payload.gdprConsent.addtlConsent).to.exist.and.to.equal(addtlConsent); expect(payload.gdprConsent.consentRequired).to.exist.and.to.be.true; }); it('Should send GPP consent data', function () { @@ -312,14 +330,159 @@ describe('onetag', function () { expect(payload.usPrivacy).to.exist; expect(payload.usPrivacy).to.exist.and.to.equal(consentString); }); + it('Should send FPD (ortb2 field)', function () { + const firtPartyData = { + // this is where the contextual data is placed + site: { + name: 'example', + domain: 'page.example.com', + // OpenRTB 2.5 spec / Content Taxonomy + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + data: [{ + name: 'www.dataprovider1.com', // who resolved the segments + ext: { + segtax: 7, // taxonomy used to encode the segments + cids: ['iris_c73g5jq96mwso4d8'] + }, + // the bare minimum are the IDs. These IDs are the ones from the new IAB Content Taxonomy v3 + segment: [ { id: '687' }, { id: '123' } ] + }] + }, + ext: { + data: { // fields that aren't part of openrtb 2.6 + pageType: 'article', + category: 'repair' + } + } + }, + // this is where the user data is placed + user: { + keywords: 'a,b', + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '1' + }] + }], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + }, + regs: { + gpp: 'abc1234', + gpp_sid: [7] + } + }; + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': firtPartyData + } + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + expect(payload.ortb2).to.exist; + expect(payload.ortb2).to.exist.and.to.deep.equal(firtPartyData); + }); + it('Should send DSA (ortb2 field)', function () { + const dsa = { + 'regs': { + 'ext': { + 'dsa': { + 'required': 1, + 'pubrender': 0, + 'datatopub': 1, + 'transparency': [{ + 'domain': 'dsa-domain', + 'params': [1, 2] + }] + } + } + } + }; + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': dsa + } + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + expect(payload.ortb2).to.exist; + expect(payload.ortb2).to.exist.and.to.deep.equal(dsa); + }); + it('Should send FLEDGE eligibility flag when FLEDGE is enabled', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'fledgeEnabled': true + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + }); + it('Should send FLEDGE eligibility flag when FLEDGE is not enabled', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'fledgeEnabled': false + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + }); + it('Should send FLEDGE eligibility flag set to false when fledgeEnabled is not defined', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(false); + }); }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); const response = getBannerVideoResponse(); + const fledgeResponse = getFledgeBannerResponse(); const requestData = JSON.parse(request.data); it('Returns an array of valid server responses if response object is valid', function () { const interpretedResponse = spec.interpretResponse(response, request); + const fledgeInterpretedResponse = spec.interpretResponse(fledgeResponse, request); expect(interpretedResponse).to.be.an('array').that.is.not.empty; + expect(fledgeInterpretedResponse).to.be.an('object'); + expect(fledgeInterpretedResponse.bids).to.satisfy(function (value) { + return value === null || Array.isArray(value); + }); + expect(fledgeInterpretedResponse.fledgeAuctionConfigs).to.be.an('array').that.is.not.empty; for (let i = 0; i < interpretedResponse.length; i++) { let dataItem = interpretedResponse[i]; expect(dataItem).to.include.all.keys('requestId', 'cpm', 'width', 'height', 'ttl', 'creativeId', 'netRevenue', 'currency', 'meta', 'dealId'); @@ -354,6 +517,21 @@ describe('onetag', function () { const serverResponses = spec.interpretResponse('invalid_response', { data: '{}' }); expect(serverResponses).to.be.an('array').that.is.empty; }); + it('Returns meta dsa field if dsa field is present in response', function () { + const dsaResponseObj = { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': { + 'domain': 'dsp1domain.com', + 'params': [1, 2] + }, + 'adrender': 1 + }; + const responseWithDsa = {...response}; + responseWithDsa.body.bids.forEach(bid => bid.dsa = {...dsaResponseObj}); + const serverResponse = spec.interpretResponse(responseWithDsa, request); + serverResponse.forEach(bid => expect(bid.meta.dsa).to.deep.equals(dsaResponseObj)); + }); }); describe('getUserSyncs', function () { const sync_endpoint = 'https://onetag-sys.com/usync/'; @@ -517,6 +695,24 @@ function getBannerVideoResponse() { }; } +function getFledgeBannerResponse() { + const bannerVideoResponse = getBannerVideoResponse(); + bannerVideoResponse.body.fledgeAuctionConfigs = [ + { + bidId: 'fledge', + config: { + seller: 'https://onetag-sys.com', + decisionLogicUrl: + 'https://onetag-sys.com/paapi/decision_logic.js', + interestGroupBuyers: [ + 'https://onetag-sys.com' + ], + } + } + ] + return bannerVideoResponse; +} + function getBannerVideoRequest() { return { data: JSON.stringify({ diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index 2515c713b14..1224c3f0740 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -663,7 +663,7 @@ describe('oolo Prebid Analytic', () => { events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); - expect(server.requests[3].url).to.equal('https://pbjs.com') + expect(server.requests[3].url).to.equal('https://pbjs.com/') }) it('should send raw events based on server configuration', () => { diff --git a/test/spec/modules/openwebBidAdapter_spec.js b/test/spec/modules/openwebBidAdapter_spec.js index c515c21690a..5a0264f00b8 100644 --- a/test/spec/modules/openwebBidAdapter_spec.js +++ b/test/spec/modules/openwebBidAdapter_spec.js @@ -2,386 +2,624 @@ import { expect } from 'chai'; import { spec } from 'modules/openwebBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; -const DEFAULT_ADATPER_REQ = { bidderCode: 'openweb' }; -const DISPLAY_REQUEST = { - 'bidder': 'openweb', - 'params': { - 'aid': 12345 - }, - 'schain': { ver: 1 }, - 'userId': { criteo: 2 }, - 'mediaTypes': { 'banner': { 'sizes': [300, 250] } }, - 'bidderRequestId': '7101db09af0db2', - 'auctionId': '2e41f65424c87c', - 'adUnitCode': 'adunit-code', - 'bidId': '84ab500420319d', -}; - -const VIDEO_REQUEST = { - 'bidder': 'openweb', - 'mediaTypes': { - 'video': { - 'playerSize': [[480, 360], [640, 480]] - } - }, - 'params': { - 'aid': 12345 - }, - 'bidderRequestId': '7101db09af0db2', - 'auctionId': '2e41f65424c87c', - 'adUnitCode': 'adunit-code', - 'bidId': '84ab500420319d' -}; - -const ADPOD_REQUEST = { - 'bidder': 'openweb', - 'mediaTypes': { - 'video': { - 'context': 'adpod', - 'playerSize': [[640, 480]], - 'anyField': 10 - } - }, - 'params': { - 'aid': 12345 - }, - 'bidderRequestId': '7101db09af0db2', - 'auctionId': '2e41f65424c87c', - 'adUnitCode': 'adunit-code', - 'bidId': '2e41f65424c87c' -}; - -const SERVER_VIDEO_RESPONSE = { - 'source': { 'aid': 12345, 'pubId': 54321 }, - 'bids': [{ - 'vastUrl': 'vastUrl', - 'requestId': '2e41f65424c87c', - 'url': '44F2AEB9BFC881B3', - 'creative_id': 342516, - 'durationSeconds': 30, - 'cmpId': 342516, - 'height': 480, - 'cur': 'USD', - 'width': 640, - 'cpm': 0.9, - 'adomain': ['a.com'] - }] -}; -const SERVER_OUSTREAM_VIDEO_RESPONSE = SERVER_VIDEO_RESPONSE; -const SERVER_DISPLAY_RESPONSE = { - 'source': { 'aid': 12345, 'pubId': 54321 }, - 'bids': [{ - 'ad': '', - 'adUrl': 'adUrl', - 'requestId': '2e41f65424c87c', - 'creative_id': 342516, - 'cmpId': 342516, - 'height': 250, - 'cur': 'USD', - 'width': 300, - 'cpm': 0.9 - }], - 'cookieURLs': ['link1', 'link2'] -}; -const SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS = { - 'source': { 'aid': 12345, 'pubId': 54321 }, - 'bids': [{ - 'ad': '', - 'requestId': '2e41f65424c87c', - 'creative_id': 342516, - 'cmpId': 342516, - 'height': 250, - 'cur': 'USD', - 'width': 300, - 'cpm': 0.9 - }], - 'cookieURLs': ['link3', 'link4'], - 'cookieURLSTypes': ['image', 'iframe'] -}; - -const videoBidderRequest = { - bidderCode: 'bidderCode', - bids: [{ mediaTypes: { video: {} }, bidId: '2e41f65424c87c' }] -}; - -const displayBidderRequest = { - bidderCode: 'bidderCode', - bids: [{ bidId: '2e41f65424c87c' }] -}; - -const displayBidderRequestWithConsents = { - bidderCode: 'bidderCode', - bids: [{ bidId: '2e41f65424c87c' }], - gdprConsent: { - gdprApplies: true, - consentString: 'test' - }, - uspConsent: 'iHaveIt' -}; - -const videoEqResponse = [{ - vastUrl: 'vastUrl', - requestId: '2e41f65424c87c', - creativeId: 342516, - mediaType: 'video', - netRevenue: true, - currency: 'USD', - height: 480, - width: 640, - ttl: 300, - cpm: 0.9, - meta: { - advertiserDomains: ['a.com'] - } -}]; - -const displayEqResponse = [{ - requestId: '2e41f65424c87c', - creativeId: 342516, - mediaType: 'banner', - netRevenue: true, - currency: 'USD', - ad: '', - adUrl: 'adUrl', - height: 250, - width: 300, - ttl: 300, - cpm: 0.9, - meta: { - advertiserDomains: [] - } - -}]; - -describe('openwebBidAdapter', () => { +const ENDPOINT = 'https://hb.openwebmp.com/hb-multi'; +const TEST_ENDPOINT = 'https://hb.openwebmp.com/hb-multi-test'; +const TTL = 360; +/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ + +describe('openwebAdapter', function () { const adapter = newBidder(spec); - describe('inherited functions', () => { - it('exists and is a function', () => { + + describe('inherited functions', function () { + it('exists and is a function', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); }); - describe('user syncs', () => { - describe('as image', () => { - it('should be returned if pixel enabled', () => { - const syncs = spec.getUserSyncs({ pixelEnabled: true }, [{ body: SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS }]); - - expect(syncs.map(s => s.url)).to.deep.equal([SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS.cookieURLs[0]]); - expect(syncs.map(s => s.type)).to.deep.equal(['image']); - }) - }) + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); - describe('as iframe', () => { - it('should be returned if iframe enabled', () => { - const syncs = spec.getUserSyncs({ iframeEnabled: true }, [{ body: SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS }]); + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); - expect(syncs.map(s => s.url)).to.deep.equal([SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS.cookieURLs[1]]); - expect(syncs.map(s => s.type)).to.deep.equal(['iframe']); - }) - }) + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + } + }, + 'vastXml': '"..."' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'loop': 2, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'openweb', + } + const placementId = '12345678'; + const api = [1, 2]; + const mimes = ['application/javascript', 'video/mp4', 'video/quicktime']; + const protocols = [2, 3, 5, 6]; + + it('sends the placementId to ENDPOINT via POST', function () { + bidRequests[0].params.placementId = placementId; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].placementId).to.equal(placementId); + }); - describe('user sync', () => { - it('should not be returned if passed syncs where already used', () => { - const syncs = spec.getUserSyncs({ - iframeEnabled: true, - pixelEnabled: true - }, [{ body: SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS }]); + it('sends the plcmt to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].plcmt).to.equal(1); + }); - expect(syncs).to.deep.equal([]); - }) + it('sends the is_wrapper parameter to ENDPOINT via POST', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('is_wrapper'); + expect(request.data.params.is_wrapper).to.equal(false); + }); - it('should not be returned if pixel not set', () => { - const syncs = spec.getUserSyncs({}, [{ body: SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS }]); + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); - expect(syncs).to.be.empty; - }); + it('sends bid request to TEST ENDPOINT via POST', function () { + const request = spec.buildRequests(testModeBidRequests, bidderRequest); + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('POST'); }); - describe('user syncs with both types', () => { - it('should be returned if pixel and iframe enabled', () => { - const mockedServerResponse = Object.assign({}, SERVER_DISPLAY_RESPONSE_WITH_MIXED_SYNCS, { 'cookieURLs': ['link5', 'link6'] }); - const syncs = spec.getUserSyncs({ - iframeEnabled: true, - pixelEnabled: true - }, [{ body: mockedServerResponse }]); - expect(syncs.map(s => s.url)).to.deep.equal(mockedServerResponse.cookieURLs); - expect(syncs.map(s => s.type)).to.deep.equal(mockedServerResponse.cookieURLSTypes); - }); + it('should send the correct bid Id', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); }); - }); - describe('isBidRequestValid', () => { - it('should return true when required params found', () => { - expect(spec.isBidRequestValid(VIDEO_REQUEST)).to.equal(true); + it('should send the correct supported api array', function () { + bidRequests[0].mediaTypes.video.api = api; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].api).to.be.an('array'); + expect(request.data.bids[0].api).to.eql([1, 2]); }); - it('should return false when required params are not passed', () => { - let bid = Object.assign({}, VIDEO_REQUEST); - delete bid.params; - expect(spec.isBidRequestValid(bid)).to.equal(false); + it('should send the correct mimes array', function () { + bidRequests[1].mediaTypes.banner.mimes = mimes; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[1].mimes).to.be.an('array'); + expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); }); - }); - describe('buildRequests', () => { - let videoBidRequests = [VIDEO_REQUEST]; - let displayBidRequests = [DISPLAY_REQUEST]; - let videoAndDisplayBidRequests = [DISPLAY_REQUEST, VIDEO_REQUEST]; - const displayRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); - const videoRequest = spec.buildRequests(videoBidRequests, DEFAULT_ADATPER_REQ); - const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, DEFAULT_ADATPER_REQ); - - it('building requests as arrays', () => { - expect(videoRequest).to.be.a('array'); - expect(displayRequest).to.be.a('array'); - expect(videoAndDisplayRequests).to.be.a('array'); - }) + it('should send the correct protocols array', function () { + bidRequests[0].mediaTypes.video.protocols = protocols; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].protocols).to.be.an('array'); + expect(request.data.bids[0].protocols).to.eql([2, 3, 5, 6]); + }); - it('sending as POST', () => { - const postActionMethod = 'POST' - const comparator = br => br.method === postActionMethod; - expect(videoRequest.every(comparator)).to.be.true; - expect(displayRequest.every(comparator)).to.be.true; - expect(videoAndDisplayRequests.every(comparator)).to.be.true; - }); - it('forms correct ADPOD request', () => { - 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'); - expect(impRequest.Adpod.anyField).to.be.equal(10); - }) - it('sends correct video bid parameters', () => { - const data = videoRequest[0].data; - - const eq = { - CallbackId: '84ab500420319d', - AdType: 'video', - Aid: 12345, - Sizes: '480x360,640x480', - PlacementId: 'adunit-code' - }; - expect(data.BidRequests[0]).to.deep.equal(eq); + it('should send the correct sizes array', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sizes).to.be.an('array'); + expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) + expect(request.data.bids[1].sizes).to.be.an('array'); + expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) }); - it('sends correct display bid parameters', () => { - const data = displayRequest[0].data; + it('should send the correct media type', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].mediaType).to.equal(VIDEO) + expect(request.data.bids[1].mediaType).to.equal(BANNER) + }); - const eq = { - CallbackId: '84ab500420319d', - AdType: 'display', - Aid: 12345, - Sizes: '300x250', - PlacementId: 'adunit-code' + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); - expect(data.BidRequests[0]).to.deep.equal(eq); - }); - - it('sends correct video and display bid parameters', () => { - const bidRequests = videoAndDisplayRequests[0].data; - const expectedBidReqs = [{ - CallbackId: '84ab500420319d', - AdType: 'display', - Aid: 12345, - Sizes: '300x250', - PlacementId: 'adunit-code' - }, { - CallbackId: '84ab500420319d', - AdType: 'video', - Aid: 12345, - Sizes: '480x360,640x480', - PlacementId: 'adunit-code' - }] + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); - expect(bidRequests.BidRequests).to.deep.equal(expectedBidReqs); + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); }); - describe('publisher environment', () => { - const sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'coppa': true - }; - return config[key]; + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } }); - const bidRequestWithPubSettingsData = spec.buildRequests([DISPLAY_REQUEST], displayBidderRequestWithConsents)[0].data; - sandbox.restore(); - it('sets GDPR', () => { - expect(bidRequestWithPubSettingsData.GDPR).to.be.equal(1); - expect(bidRequestWithPubSettingsData.GDPRConsent).to.be.equal(displayBidderRequestWithConsents.gdprConsent.consentString); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); + config.setConfig({ + userSync: { + syncEnabled: true, + } }); - it('sets USP', () => { - expect(bidRequestWithPubSettingsData.USP).to.be.equal(displayBidderRequestWithConsents.uspConsent); - }) - it('sets Coppa', () => { - expect(bidRequestWithPubSettingsData.Coppa).to.be.equal(1); - }) - it('sets Schain', () => { - expect(bidRequestWithPubSettingsData.Schain).to.be.deep.equal(DISPLAY_REQUEST.schain); - }) - it('sets UserId\'s', () => { - expect(bidRequestWithPubSettingsData.UserIds).to.be.deep.equal(DISPLAY_REQUEST.userId); - }) - }) - }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); + }); - describe('interpretResponse', () => { - let serverResponse; - let adapterRequest; - let eqResponse; + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); - afterEach(() => { - serverResponse = null; - adapterRequest = null; - eqResponse = null; + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); }); - it('should get correct video bid response', () => { - serverResponse = SERVER_VIDEO_RESPONSE; - adapterRequest = videoBidderRequest; - eqResponse = videoEqResponse; + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); + }); - bidServerResponseCheck(); + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); }); - it('should get correct display bid response', () => { - serverResponse = SERVER_DISPLAY_RESPONSE; - adapterRequest = displayBidderRequest; - eqResponse = displayEqResponse; + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); + }); - bidServerResponseCheck(); + it('should not send the gpp param if gppConsent is false in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: false}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gpp'); + expect(request.data.params).to.not.have.property('gpp_sid'); }); - function bidServerResponseCheck() { - const result = spec.interpretResponse({ body: serverResponse }, { adapterRequest }); + it('should send the gpp param if gppConsent is true in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: {gppString: 'test-consent-string', applicableSections: [7]}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gpp', 'test-consent-string'); + expect(request.data.params.gpp_sid[0]).to.be.equal(7); + }); - expect(result).to.deep.equal(eqResponse); - } + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + }); + + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); + }); - function nobidServerResponseCheck() { - const noBidServerResponse = { bids: [] }; - const noBidResult = spec.interpretResponse({ body: noBidServerResponse }, { adapterRequest }); + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); + }); - expect(noBidResult.length).to.equal(0); - } + it('should check sua param in bid request', function() { + const sua = { + 'platform': { + 'brand': 'macOS', + 'version': ['12', '4', '0'] + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Not;A=Brand', + 'version': [ '99', '0', '0', '0' ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'device': { + 'sua': { + 'platform': { + 'brand': 'macOS', + 'version': [ '12', '4', '0' ] + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Not;A=Brand', + 'version': [ '99', '0', '0', '0' ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + } + } + const requestWithSua = spec.buildRequests([bid], bidderRequest); + const data = requestWithSua.data; + expect(data.bids[0].sua).to.exist; + expect(data.bids[0].sua).to.deep.equal(sua); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sua).to.not.exist; + }); + + describe('COPPA Param', function() { + it('should set coppa equal 0 in bid request if coppa is set to false', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(0); + }); + + it('should set coppa equal 1 in bid request if coppa is set to true', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'regs': { + 'coppa': true, + } + }; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(1); + }); + }); + }); + + describe('interpretResponse', function () { + const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [{ + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: VIDEO + }, + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER + }] + }; + + const expectedVideoResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 640, + height: 480, + ttl: TTL, + creativeId: '21e12606d47ba7', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: VIDEO, + meta: { + mediaType: VIDEO, + advertiserDomains: ['abc.com'] + }, + vastXml: '', + }; + + const expectedBannerResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 640, + height: 480, + ttl: TTL, + creativeId: '21e12606d47ba7', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: BANNER, + meta: { + mediaType: BANNER, + advertiserDomains: ['abc.com'] + }, + ad: '""' + }; + + it('should get correct bid response', function () { + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); + expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + }); + + it('video type should have vastXml key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[0].vastXml).to.equal(expectedVideoResponse.vastXml) + }); + + it('banner type should have ad key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[1].ad).to.equal(expectedBannerResponse.ad) + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + } + }; + + const iframeSyncResponse = { + body: { + params: { + userSyncURL: 'https://iframe-sync-url.test' + } + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); - it('handles video nobid responses', () => { - adapterRequest = videoBidderRequest; + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); - nobidServerResponseCheck(); + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); }); - it('handles display nobid responses', () => { - adapterRequest = displayBidderRequest; + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); - nobidServerResponseCheck(); + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); }); + }) - it('forms correct ADPOD response', () => { - const videoBids = spec.interpretResponse({ body: SERVER_VIDEO_RESPONSE }, { adapterRequest: { bids: [ADPOD_REQUEST] } }); - expect(videoBids[0].video.durationSeconds).to.be.equal(30); - expect(videoBids[0].video.context).to.be.equal('adpod'); + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) }) - }); + }) }); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index f2cff7f470c..7c504bca50b 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -14,6 +14,7 @@ import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import {deepClone} from 'src/utils.js'; +import {version} from 'package.json'; import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {hook} from '../../../src/hook.js'; @@ -316,6 +317,7 @@ describe('OpenxRtbAdapter', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); expect(request[0].url).to.equal(REQUEST_URL); expect(request[0].method).to.equal('POST'); + expect(request[0].data.ext.pv).to.equal(version); }); it('should send delivery domain, if available', function () { @@ -1506,6 +1508,25 @@ describe('OpenxRtbAdapter', function () { expect(response.fledgeAuctionConfigs.length).to.equal(1); expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test-bid-id'); }); + + it('should inject ortb2Imp in auctionSignals', function () { + const auctionConfig = response.fledgeAuctionConfigs[0].config; + expect(auctionConfig).to.deep.include({ + auctionSignals: { + ortb2Imp: { + id: 'test-bid-id', + tagid: '12345678', + banner: { + topframe: 0, + format: bidRequestConfigs[0].mediaTypes.banner.sizes.map(([w, h]) => ({w, h})) + }, + ext: { + divid: 'adunit-code', + } + } + } + }); + }) }); }); diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index 37d4a2c7bc0..9a8981235d5 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -266,6 +266,95 @@ describe('Opera Ads Bid Adapter', function () { } }); + describe('test fulfilling inventory information', function () { + const bidRequest = { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: {sizes: [[300, 250]]} + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + } + + const getRequest = function () { + let reqs; + expect(function () { + reqs = spec.buildRequests([bidRequest], bidderRequest); + }).to.not.throw(); + return JSON.parse(reqs[0].data); + } + + it('test default case', function () { + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.domain).to.not.be.empty; + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with site information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-1', + domain: 'www.test.com' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-1'); + expect(requestData.site.domain).to.equal('www.test.com'); + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.app).to.be.an('object'); + expect(requestData.app.id).to.equal(bidRequest.params.publisherId); + expect(requestData.app.name).to.equal('test-app-1'); + expect(requestData.app.domain).to.not.be.empty; + }); + + it('test a case with both site and app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-2', + page: 'test-page' + }, + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-2'); + expect(requestData.site.page).to.equal('test-page'); + expect(requestData.site.domain).to.not.be.empty; + }); + }); + it('test getBidFloor', function() { const bidRequests = [ { diff --git a/test/spec/modules/operaadsIdSystem_spec.js b/test/spec/modules/operaadsIdSystem_spec.js new file mode 100644 index 00000000000..d81f643d62f --- /dev/null +++ b/test/spec/modules/operaadsIdSystem_spec.js @@ -0,0 +1,53 @@ +import { operaIdSubmodule } from 'modules/operaadsIdSystem' +import * as ajaxLib from 'src/ajax.js' + +const TEST_ID = 'opera-test-id'; +const operaIdRemoteResponse = { uid: TEST_ID }; + +describe('operaId submodule properties', () => { + it('should expose a "name" property equal to "operaId"', () => { + expect(operaIdSubmodule.name).to.equal('operaId'); + }); +}); + +function fakeRequest(fn) { + const ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { + return (url, cbObj) => { + cbObj.success(JSON.stringify(operaIdRemoteResponse)); + } + }); + fn(); + ajaxBuilderStub.restore(); +} + +describe('operaId submodule getId', function() { + it('request to the fake server to correctly extract test ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: { pid: 'pub123' } }); + moduleIdCallbackResponse.callback((id) => { + expect(id).to.equal(operaIdRemoteResponse.operaId); + }); + }); + }); + + it('request to the fake server without publiser ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: {} }); + expect(moduleIdCallbackResponse).to.equal(undefined); + }); + }); +}); + +describe('operaId submodule decode', function() { + it('should respond with an object containing "operaId" as key with the value', () => { + expect(operaIdSubmodule.decode(TEST_ID)).to.deep.equal({ + operaId: TEST_ID + }); + }); + + it('should respond with undefined if the value is not a string or an empty string', () => { + [1, 2.0, null, undefined, NaN, [], {}].forEach((value) => { + expect(operaIdSubmodule.decode(value)).to.equal(undefined); + }); + }); +}); diff --git a/test/spec/modules/opscoBidAdapter_spec.js b/test/spec/modules/opscoBidAdapter_spec.js new file mode 100644 index 00000000000..38cacff8f82 --- /dev/null +++ b/test/spec/modules/opscoBidAdapter_spec.js @@ -0,0 +1,260 @@ +import {expect} from 'chai'; +import {spec} from 'modules/opscoBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('opscoBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + }) + + describe('isBidRequestValid', function () { + const validBid = { + bidder: 'opsco', + params: { + placementId: '123', + publisherId: '456' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }; + + it('should return true when required params are present', function () { + expect(spec.isBidRequestValid(validBid)).to.be.true; + }); + + it('should return false when placementId is missing', function () { + const invalidBid = {...validBid}; + delete invalidBid.params.placementId; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('should return false when publisherId is missing', function () { + const invalidBid = {...validBid}; + delete invalidBid.params.publisherId; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('should return false when mediaTypes.banner.sizes is missing', function () { + const invalidBid = {...validBid}; + delete invalidBid.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('should return false when mediaTypes.banner is missing', function () { + const invalidBid = {...validBid}; + delete invalidBid.mediaTypes.banner; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('should return false when bid params are missing', function () { + const invalidBid = {bidder: 'opsco'}; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('should return false when bid params are empty', function () { + const invalidBid = {bidder: 'opsco', params: {}}; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let validBid, bidderRequest; + + beforeEach(function () { + validBid = { + bidder: 'opsco', + params: { + placementId: '123', + publisherId: '456' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }; + + bidderRequest = { + bidderRequestId: 'bid123', + refererInfo: { + domain: 'example.com', + page: 'https://example.com/page', + ref: 'https://referrer.com' + }, + gdprConsent: { + consentString: 'GDPR_CONSENT_STRING', + gdprApplies: true + }, + }; + }); + + it('should return true when banner sizes are defined', function () { + expect(spec.isBidRequestValid(validBid)).to.be.true; + }); + + it('should return false when banner sizes are invalid', function () { + const invalidSizes = [ + '2:1', + undefined, + 123, + 'undefined' + ]; + + invalidSizes.forEach((sizes) => { + validBid.mediaTypes.banner.sizes = sizes; + expect(spec.isBidRequestValid(validBid)).to.be.false; + }); + }); + + it('should send GDPR consent in the payload if present', function () { + const request = spec.buildRequests([validBid], bidderRequest); + expect(JSON.parse(request.data).user.ext.consent).to.deep.equal('GDPR_CONSENT_STRING'); + }); + + it('should send CCPA in the payload if present', function () { + const ccpa = '1YYY'; + bidderRequest.uspConsent = ccpa; + const request = spec.buildRequests([validBid], bidderRequest); + expect(JSON.parse(request.data).regs.ext.us_privacy).to.equal(ccpa); + }); + + it('should send eids in the payload if present', function () { + const eids = {data: [{source: 'test', uids: [{id: '123', ext: {}}]}]}; + validBid.userIdAsEids = eids; + const request = spec.buildRequests([validBid], bidderRequest); + expect(JSON.parse(request.data).user.ext.eids).to.deep.equal(eids); + }); + + it('should send schain in the payload if present', function () { + const schain = {'ver': '1.0', 'complete': 1, 'nodes': [{'asi': 'exchange1.com', 'sid': '1234', 'hp': 1}]}; + validBid.schain = schain; + const request = spec.buildRequests([validBid], bidderRequest); + expect(JSON.parse(request.data).source.ext.schain).to.deep.equal(schain); + }); + + it('should correctly identify test mode', function () { + validBid.params.test = true; + const request = spec.buildRequests([validBid], bidderRequest); + expect(JSON.parse(request.data).test).to.equal(1); + }); + }); + + describe('interpretResponse', function () { + const validResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: 'bid1', + price: 1.5, + w: 300, + h: 250, + crid: 'creative1', + currency: 'USD', + netRevenue: true, + ttl: 300, + adm: '
Ad content
', + mtype: 1 + }, + { + impid: 'bid2', + price: 2.0, + w: 728, + h: 90, + crid: 'creative2', + currency: 'USD', + netRevenue: true, + ttl: 300, + adm: '
Ad content
', + mtype: 1 + } + ] + } + ] + } + }; + + const emptyResponse = { + body: { + seatbid: [] + } + }; + + it('should return an array of bid objects with valid response', function () { + const interpretedBids = spec.interpretResponse(validResponse); + const expectedBids = validResponse.body.seatbid[0].bid; + expect(interpretedBids).to.have.lengthOf(expectedBids.length); + expectedBids.forEach((expectedBid, index) => { + expect(interpretedBids[index]).to.have.property('requestId', expectedBid.impid); + expect(interpretedBids[index]).to.have.property('cpm', expectedBid.price); + expect(interpretedBids[index]).to.have.property('width', expectedBid.w); + expect(interpretedBids[index]).to.have.property('height', expectedBid.h); + expect(interpretedBids[index]).to.have.property('creativeId', expectedBid.crid); + expect(interpretedBids[index]).to.have.property('currency', expectedBid.currency); + expect(interpretedBids[index]).to.have.property('netRevenue', expectedBid.netRevenue); + expect(interpretedBids[index]).to.have.property('ttl', expectedBid.ttl); + expect(interpretedBids[index]).to.have.property('ad', expectedBid.adm); + expect(interpretedBids[index]).to.have.property('mediaType', expectedBid.mtype); + }); + }); + + it('should return an empty array with empty response', function () { + const interpretedBids = spec.interpretResponse(emptyResponse); + expect(interpretedBids).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + const RESPONSE = { + body: { + ext: { + usersync: { + sovrn: { + syncs: [{type: 'iframe', url: 'https://sovrn.com/iframe_sync'}] + }, + appnexus: { + syncs: [{type: 'image', url: 'https://appnexus.com/image_sync'}] + } + } + } + } + }; + + it('should return empty array if no options are provided', function () { + const opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty array if neither iframe nor pixel is enabled', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return syncs only for iframe sync type', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync.sovrn.syncs[0].url); + }); + + it('should return syncs only for pixel sync types', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync.appnexus.syncs[0].url); + }); + + it('should return syncs when both iframe and pixel are enabled', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); + expect(opts.length).to.equal(2); + }); + }); +}); diff --git a/test/spec/modules/optidigitalBidAdapter_spec.js b/test/spec/modules/optidigitalBidAdapter_spec.js index caa12483ea9..30e72452c39 100755 --- a/test/spec/modules/optidigitalBidAdapter_spec.js +++ b/test/spec/modules/optidigitalBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/optidigitalBidAdapter.js'; +import { spec, resetSync } from 'modules/optidigitalBidAdapter.js'; import * as utils from 'src/utils.js'; const ENDPOINT = 'https://pbs.optidigital.com/bidder'; @@ -479,6 +479,28 @@ describe('optidigitalAdapterTests', function () { expect(payload.imp[0].bidFloor).to.exist; }); + it('should add userEids to payload', function() { + const userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: '121213434342343', + atype: 1 + }] + }]; + validBidRequests[0].userIdAsEids = userIdAsEids; + bidderRequest.userIdAsEids = userIdAsEids; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user.eids).to.deep.equal(userIdAsEids); + }); + + it('should not add userIdAsEids to payload when userIdAsEids is not present', function() { + validBidRequests[0].userIdAsEids = undefined; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user).to.deep.equal(undefined); + }); + function returnBannerSizes(mediaTypes, expectedSizes) { const bidRequest = Object.assign(validBidRequests[0], mediaTypes); const request = spec.buildRequests([bidRequest], bidderRequest); @@ -497,6 +519,7 @@ describe('optidigitalAdapterTests', function () { let test; beforeEach(function () { test = sinon.sandbox.create(); + resetSync(); }); afterEach(function() { test.restore(); @@ -508,16 +531,22 @@ describe('optidigitalAdapterTests', function () { }]); }); - it('should return appropriate URL', function() { + it('should return appropriate URL with GDPR equals to 1 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 0 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=0&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 1 and no consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=` }]); + }); + it('should return appropriate URL with GDPR equals to 1, GDPR consent and CCPA consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, {consentString: 'fooUsp'})).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo&ccpa_consent=fooUsp` }]); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index aec8b79045e..8a9f000bbb9 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -21,13 +21,80 @@ describe('Optimera RTD sub module', () => { }); }); -describe('Optimera RTD score file url is properly set', () => { - it('Proerly set the score file url', () => { +describe('Optimera RTD score file URL is properly set for v0', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v0', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL without apiVersion set', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL with an api version other than v0 or v1', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v15', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); }); }); +describe('Optimera RTD score file URL is properly set for v1', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v1', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v1'); + expect(optimeraRTD.scoresURL).to.equal('https://v1.oapi26b.com/api/products/scores?c=9999&h=localhost:9876&p=/context.html&s=de'); + }); +}); + describe('Optimera RTD score file properly sets targeting values', () => { const scores = { 'div-0': ['A1', 'A2'], diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index acb779b436d..cf58d35e636 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; -import {spec} from 'modules/orbidderBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; +import { expect } from 'chai'; +import { spec } from 'modules/orbidderBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import * as _ from 'lodash'; import { BANNER, NATIVE } from '../../../src/mediaTypes.js'; @@ -9,39 +9,57 @@ describe('orbidderBidAdapter', () => { const defaultBidRequestBanner = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bb', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d8', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c6', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code', sizes: [[300, 250], [300, 600]], params: { 'accountId': 'string1', - 'placementId': 'string2' + 'placementId': 'string2', + 'bidfloor': 1.23 }, mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]], + sizes: [[300, 250], [300, 600]] } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*XXXXXXXXXXXXX', + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*XXXXXXXXXXXXX', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + ] + } + ] }; const defaultBidRequestNative = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bc', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d9', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c7', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code-native', sizes: [], params: { 'accountId': 'string3', - 'placementId': 'string4' + 'placementId': 'string4', + 'bidfloor': 2.34 }, mediaTypes: { native: { @@ -56,10 +74,34 @@ describe('orbidderBidAdapter', () => { required: true } } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*YYYYYYYYYYYYYYY', + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*YYYYYYYYYYYYYYY', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + ] + } + ] }; - const deepClone = function (val) { + const deepClone = function(val) { return JSON.parse(JSON.stringify(val)); }; @@ -91,15 +133,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(defaultBidRequestNative)).to.equal(true); }); - it('banner: accepts optional profile object', () => { + it('banner: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); - it('native: accepts optional profile object', () => { + it('native: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); @@ -115,15 +157,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('banner: doesn\'t accept malformed profile', () => { + it('banner: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('native: doesn\'t accept malformed profile', () => { + it('native: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); @@ -179,34 +221,20 @@ describe('orbidderBidAdapter', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(request.data).length).to.equal(Object.keys(defaultBidRequestBanner).length + 2); - expect(request.data.bidId).to.equal(defaultBidRequestBanner.bidId); - expect(request.data.auctionId).to.equal(defaultBidRequestBanner.auctionId); - expect(request.data.transactionId).to.equal(defaultBidRequestBanner.ortb2Imp.ext.tid); - expect(request.data.bidRequestCount).to.equal(defaultBidRequestBanner.bidRequestCount); - expect(request.data.adUnitCode).to.equal(defaultBidRequestBanner.adUnitCode); - expect(request.data.pageUrl).to.equal('https://localhost:9876/'); - expect(request.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(request.data.sizes).to.equal(defaultBidRequestBanner.sizes); - - expect(_.isEqual(request.data.params, defaultBidRequestBanner.params)).to.be.true; - expect(_.isEqual(request.data.mediaTypes, defaultBidRequestBanner.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestBanner); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(request.data).to.deep.equal(expectedBidRequest); }); it('native: sends correct bid parameters', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(nativeRequest.data).length).to.equal(Object.keys(defaultBidRequestNative).length + 2); - expect(nativeRequest.data.bidId).to.equal(defaultBidRequestNative.bidId); - expect(nativeRequest.data.auctionId).to.equal(defaultBidRequestNative.auctionId); - expect(nativeRequest.data.transactionId).to.equal(defaultBidRequestNative.ortb2Imp.ext.tid); - expect(nativeRequest.data.bidRequestCount).to.equal(defaultBidRequestNative.bidRequestCount); - expect(nativeRequest.data.adUnitCode).to.equal(defaultBidRequestNative.adUnitCode); - expect(nativeRequest.data.pageUrl).to.equal('https://localhost:9876/'); - expect(nativeRequest.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(nativeRequest.data.sizes).to.be.empty; - - expect(_.isEqual(nativeRequest.data.params, defaultBidRequestNative.params)).to.be.true; - expect(_.isEqual(nativeRequest.data.mediaTypes, defaultBidRequestNative.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestNative); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(nativeRequest.data).to.deep.equal(expectedBidRequest); }); it('banner: handles empty gdpr object', () => { @@ -347,7 +375,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; }); @@ -387,7 +415,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); Object.keys(expectedResponse[0]).forEach((key) => { @@ -454,7 +482,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; @@ -474,7 +502,7 @@ describe('orbidderBidAdapter', () => { 'netRevenue': true, } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -492,7 +520,7 @@ describe('orbidderBidAdapter', () => { 'creativeId': '29681110', } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -518,13 +546,13 @@ describe('orbidderBidAdapter', () => { } } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); it('handles nobid responses', () => { const serverResponse = []; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); }); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index d8690aeb6a5..e6abb5e9caa 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/outbrainBidAdapter.js'; +import { spec, storage } from 'modules/outbrainBidAdapter.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr'; @@ -213,15 +213,18 @@ describe('Outbrain Adapter', function () { }) describe('buildRequests', function () { + let getDataFromLocalStorageStub; + before(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') config.setConfig({ outbrain: { bidderUrl: 'https://bidder-url.com', } - } - ) + }) }) after(() => { + getDataFromLocalStorageStub.restore() config.resetConfig() }) @@ -424,7 +427,7 @@ describe('Outbrain Adapter', function () { expect(resData.badv).to.deep.equal(['bad-advertiser']) }); - it('first party data', function () { + it('should pass first party data', function () { const bidRequest = { ...commonBidRequest, ...nativeBidRequestParams, @@ -505,6 +508,28 @@ describe('Outbrain Adapter', function () { config.resetConfig() }); + it('should pass gpp information', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + }; + const bidderRequest = { + ...commonBidderRequest, + 'gppConsent': { + 'gppString': 'abc12345', + 'applicableSections': [8] + } + } + + const res = spec.buildRequests([bidRequest], bidderRequest); + const resData = JSON.parse(res.data); + + expect(resData.regs.ext.gpp).to.exist; + expect(resData.regs.ext.gpp_sid).to.exist; + expect(resData.regs.ext.gpp).to.equal('abc12345'); + expect(resData.regs.ext.gpp_sid).to.deep.equal([8]); + }); + it('should pass extended ids', function () { let bidRequest = { bidId: 'bidId', @@ -522,6 +547,22 @@ describe('Outbrain Adapter', function () { ]); }); + it('should pass OB user token', function () { + getDataFromLocalStorageStub.returns('12345'); + + let bidRequest = { + bidId: 'bidId', + params: {}, + ...commonBidRequest, + }; + + let res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data) + expect(resData.user.ext.obusertoken).to.equal('12345') + expect(getDataFromLocalStorageStub.called).to.be.true; + sinon.assert.calledWith(getDataFromLocalStorageStub, 'OB-USER-TOKEN'); + }); + it('should pass bidfloor', function () { const bidRequest = { ...commonBidRequest, @@ -842,6 +883,12 @@ describe('Outbrain Adapter', function () { type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); + + it('should pass gpp consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '', { gppString: 'abc12345', applicableSections: [1, 2] })).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gpp=abc12345&gpp_sid=1%2C2` + }]); + }); }) describe('onBidWon', function () { diff --git a/test/spec/modules/oxxionAnalyticsAdapter_spec.js b/test/spec/modules/oxxionAnalyticsAdapter_spec.js index 5516fb83320..9d06be24f68 100644 --- a/test/spec/modules/oxxionAnalyticsAdapter_spec.js +++ b/test/spec/modules/oxxionAnalyticsAdapter_spec.js @@ -86,20 +86,21 @@ describe('Oxxion Analytics', function () { } }, 'adUnitCode': 'tag_200124_banner', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'sizes': [ [ 300, 600 ] ], - 'bidId': '34a63e5d5378a3', + 'bidId': '2bd3e8ff8a113f', 'bidderRequestId': '11dc6ff6378de7', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 + 'bidderWinsCount': 0, + 'ova': 'cleared' } ], 'auctionStart': 1647424261187, @@ -149,12 +150,12 @@ describe('Oxxion Analytics', function () { 'bidsReceived': [ { 'bidderCode': 'appnexus', - 'width': 300, - 'height': 600, + 'width': 970, + 'height': 250, 'statusMessage': 'Bid available', - 'adId': '7a4ced80f33d33', - 'requestId': '34a63e5d5378a3', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'adId': '65d16ef039a97a', + 'requestId': '2bd3e8ff8a113f', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', 'mediaType': 'video', 'source': 'client', @@ -167,7 +168,8 @@ describe('Oxxion Analytics', function () { 'meta': { 'advertiserDomains': [ 'example.com' - ] + ], + 'demandSource': 'something' }, 'renderer': 'something', 'originalCpm': 25.02521, @@ -186,7 +188,7 @@ describe('Oxxion Analytics', function () { 'size': '300x600', 'adserverTargeting': { 'hb_bidder': 'appnexus', - 'hb_adid': '7a4ced80f33d33', + 'hb_adid': '65d16ef039a97a', 'hb_pb': '20.000000', 'hb_size': '300x600', 'hb_source': 'client', @@ -313,13 +315,16 @@ describe('Oxxion Analytics', function () { expect(message.auctionEnd[0].bidsReceived[0]).not.to.have.property('ad'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('meta'); expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('advertiserDomains'); + expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('demandSource'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('adId'); expect(message.auctionEnd[0]).to.have.property('bidderRequests').and.to.have.lengthOf(1); expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('gdprConsent'); expect(message.auctionEnd[0].bidderRequests[0].gdprConsent).not.to.have.property('vendorData'); + expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('oxxionMode'); }); it('test bidWon', function() { + window.OXXION_MODE = {'abtest': true}; adapterManager.registerAnalyticsAdapter({ code: 'oxxion', adapter: oxxionAnalytics @@ -337,7 +342,8 @@ describe('Oxxion Analytics', function () { expect(message).not.to.have.property('ad'); expect(message).to.have.property('adId') expect(message).to.have.property('cpmIncrement').and.to.equal(27.4276); - // sinon.assert.callCount(oxxionAnalytics.track, 1); + expect(message).to.have.property('oxxionMode').and.to.have.property('abtest').and.to.equal(true); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); }); }); diff --git a/test/spec/modules/oxxionRtdProvider_spec.js b/test/spec/modules/oxxionRtdProvider_spec.js index 7bccf2319a4..2a8024f3565 100644 --- a/test/spec/modules/oxxionRtdProvider_spec.js +++ b/test/spec/modules/oxxionRtdProvider_spec.js @@ -113,77 +113,6 @@ let bids = [{ }, ]; -let originalBidderRequests = [{ - 'bidderCode': 'rubicon', - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'bidderRequestId': '16c2bceb2e891a', - 'bids': [ - { - 'bidder': 'rubicon', - 'params': { - 'accountId': 1234, - 'siteId': 2345, - 'zoneId': 3456 - }, - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'mediaTypes': {'banner': {'sizes': [[970, 250]]}}, - 'adUnitCode': 'adunit1', - 'transactionId': '8f20b49c-5e47-4bb5-a7d5-0b816cf527f3', - 'bidId': '2d9920072ab028', - 'bidderRequestId': '16c2bceb2e891a', - }, - { - 'bidder': 'rubicon', - 'params': { - 'accountId': 1234, - 'siteId': 2345, - 'zoneId': 4567 - }, - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, - 'adUnitCode': 'adunit2', - 'transactionId': '4161f09e-7870-4486-b2a6-b4158a327bc4', - 'bidId': '331c3d708f4864', - 'bidderRequestId': '16c2bceb2e891a', - 'src': 'client', - } - ], - 'auctionStart': 1683383333809, - 'timeout': 3000, - 'gdprConsent': { - 'consentString': 'consent_hash', - 'gdprApplies': true, - 'apiVersion': 2 - } -}, -{ - 'bidderCode': 'appnexusAst', - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'bidderRequestId': '4d83b8c60d45e7', - 'bids': [ - { - 'bidder': 'appnexusAst', - 'params': { - 'placementId': 10471298 - }, - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, - 'adUnitCode': 'adunit2', - 'transactionId': '4161f09e-7870-4486-b2a6-b4158a327bc4', - 'bidId': '5b7cd5abc6aea3', - 'bidderRequestId': '4d83b8c60d45e7', - } - ], - 'auctionStart': 1683383333809, - 'timeout': 3000, - 'gdprConsent': { - 'consentString': 'consent_hash', - 'gdprApplies': true, - 'apiVersion': 2 - } -} -]; - let bidInterests = [ {'id': 0, 'rate': 50.0, 'suggestion': true}, {'id': 1, 'rate': 12.0, 'suggestion': false}, @@ -212,8 +141,6 @@ describe('oxxionRtdProvider', () => { auctionEnd.bidsReceived = bids; it('call everything', function() { oxxionSubmodule.getBidRequestData(request, null, moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[0], moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[1], moduleConfig); }); it('check bid filtering', function() { let requestsList = oxxionSubmodule.getRequestsList(request); @@ -229,27 +156,5 @@ describe('oxxionRtdProvider', () => { expect(filteredBiddderRequests[1]).to.have.property('bids'); expect(filteredBiddderRequests[1].bids.length).to.equal(1); }); - it('check vastImpUrl', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastImpUrl'); - let expectVastImpUrl = 'https://' + moduleConfig.params.domain + '.oxxion.io/analytics/vast_imp?'; - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(expectVastImpUrl); - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('https://some.tracking-url.com')); - }); - it('check vastXml', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastXml'); - let vastWrapper = new DOMParser().parseFromString(auctionEnd.bidsReceived[0].vastXml, 'text/xml'); - let impressions = vastWrapper.querySelectorAll('VAST Ad Wrapper Impression'); - expect(impressions.length).to.equal(2); - expect(auctionEnd.bidsReceived[1]).to.have.property('vastXml'); - expect(auctionEnd.bidsReceived[1].adId).to.equal('4b2e1581c0ca1a'); - let vastInline = new DOMParser().parseFromString(auctionEnd.bidsReceived[1].vastXml, 'text/xml'); - let inline = vastInline.querySelectorAll('VAST Ad InLine'); - expect(inline).to.have.lengthOf(1); - let inlineImpressions = vastInline.querySelectorAll('VAST Ad InLine Impression'); - expect(inlineImpressions).to.have.lengthOf.above(0); - }); - it('check cpmIncrement', function() { - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('cpmIncrement=0')); - }); }); }); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index 4c7e330b237..73df2fba8fd 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -6,7 +6,6 @@ import {getGranularityKeyName, getGranularityObject} from '../../../modules/ozon import * as utils from '../../../src/utils.js'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const BIDDER_CODE = 'ozone'; - var validBidRequests = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -2049,6 +2048,40 @@ describe('ozone Adapter', function () { expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); }); + it('should pass gpid to auction if it is present (gptPreAuction adapter sets this)', function () { + var specMock = utils.deepClone(spec); + let br = JSON.parse(JSON.stringify(validBidRequests)); + utils.deepSetValue(br[0], 'ortb2Imp.ext.gpid', '/22037345/projectozone'); + const request = specMock.buildRequests(br, validBidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].ext.gpid).to.equal('/22037345/projectozone'); + }); + it('should batch into 10s if config is set', function () { + config.setConfig({ozone: {'batchRequests': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 25; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.length).to.equal(3); + config.resetConfig(); + }); + it('should not batch into 10s if config is set to false and singleRequest is true', function () { + config.setConfig({ozone: {'batchRequests': false, 'singleRequest': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 15; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); + config.resetConfig(); + }); it('should use GET values auction=dev & cookiesync=dev if set', function() { var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { @@ -2429,6 +2462,12 @@ describe('ozone Adapter', function () { const result = spec.interpretResponse(validres, request); expect(utils.deepAccess(result[0].adserverTargeting, 'oz_ozappnexus_sid')).to.equal(result[0].cid); }); + it('should add oz_auc_id (response id value)', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); + }); it('should add unique adId values to each bid', function() { const request = spec.buildRequests(validBidRequests, validBidderRequest); let validres = JSON.parse(JSON.stringify(validResponse2BidsSameAdunit)); diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js new file mode 100644 index 00000000000..3d264e87e51 --- /dev/null +++ b/test/spec/modules/paapi_spec.js @@ -0,0 +1,628 @@ +import {expect} from 'chai'; +import {config} from '../../../src/config.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as utils from '../../../src/utils.js'; +import {hook} from '../../../src/hook.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import { + addComponentAuctionHook, + getPAAPIConfig, + parseExtPrebidFledge, + registerSubmodule, + setImpExtAe, + setResponseFledgeConfigs, + reset +} from 'modules/paapi.js'; +import * as events from 'src/events.js'; +import CONSTANTS from 'src/constants.json'; +import {getGlobal} from '../../../src/prebidGlobal.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {AuctionIndex} from '../../../src/auctionIndex.js'; + +describe('paapi module', () => { + let sandbox; + before(reset); + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + reset(); + }); + + [ + 'fledgeForGpt', + 'paapi' + ].forEach(configNS => { + describe(`using ${configNS} for configuration`, () => { + describe('getPAAPIConfig', function () { + let nextFnSpy, fledgeAuctionConfig; + before(() => { + config.setConfig({[configNS]: {enabled: true}}); + }); + beforeEach(() => { + fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; + nextFnSpy = sinon.spy(); + }); + + describe('on a single auction', function () { + const auctionId = 'aid'; + beforeEach(function () { + sandbox.stub(auctionManager, 'index').value(stubAuctionIndex({auctionId})); + }); + + it('should call next()', function () { + const request = {auctionId, adUnitCode: 'auc'}; + addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); + sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + }); + + describe('should collect auction configs', () => { + let cf1, cf2; + beforeEach(() => { + cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; + cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, cf1); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, cf2); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1', 'au2', 'au3']}); + }); + + it('and make them available at end of auction', () => { + sinon.assert.match(getPAAPIConfig({auctionId}), { + au1: { + componentAuctions: [cf1] + }, + au2: { + componentAuctions: [cf2] + } + }); + }); + + it('and filter them by ad unit', () => { + const cfg = getPAAPIConfig({auctionId, adUnitCode: 'au1'}); + expect(Object.keys(cfg)).to.have.members(['au1']); + sinon.assert.match(cfg.au1, { + componentAuctions: [cf1] + }); + }); + + it('and not return them again', () => { + getPAAPIConfig(); + const cfg = getPAAPIConfig(); + expect(cfg).to.eql({}); + }); + + describe('includeBlanks = true', () => { + it('includes all ad units', () => { + const cfg = getPAAPIConfig({}, true); + expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); + expect(cfg.au3).to.eql(null); + }) + it('includes the targeted adUnit', () => { + expect(getPAAPIConfig({adUnitCode: 'au3'}, true)).to.eql({ + au3: null + }) + }); + it('includes the targeted auction', () => { + const cfg = getPAAPIConfig({auctionId}, true); + expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']); + expect(cfg.au3).to.eql(null); + }); + it('does not include non-existing ad units', () => { + expect(getPAAPIConfig({adUnitCode: 'other'})).to.eql({}); + }); + it('does not include non-existing auctions', () => { + expect(getPAAPIConfig({auctionId: 'other'})).to.eql({}); + }) + }); + }); + + it('should drop auction configs after end of auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId}); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId}); + expect(getPAAPIConfig({auctionId})).to.eql({}); + }); + + it('should augment auctionSignals with FPD', () => { + addComponentAuctionHook(nextFnSpy, { + auctionId, + adUnitCode: 'au1', + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + }, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId}); + sinon.assert.match(getPAAPIConfig({auctionId}), { + au1: { + componentAuctions: [{ + ...fledgeAuctionConfig, + auctionSignals: { + prebid: { + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + } + } + }] + } + }); + }); + + describe('submodules', () => { + let submods; + beforeEach(() => { + submods = [1, 2].map(i => ({ + name: `test${i}`, + onAuctionConfig: sinon.stub() + })); + submods.forEach(registerSubmodule); + }); + + describe('onAuctionConfig', () => { + const auctionId = 'aid'; + it('is invoked with null configs when there\'s no config', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au']}); + submods.forEach(submod => sinon.assert.calledWith(submod.onAuctionConfig, auctionId, {au: null})); + }); + it('is invoked with relevant configs', () => { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au2'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1', 'au2', 'au3']}); + submods.forEach(submod => { + sinon.assert.calledWith(submod.onAuctionConfig, auctionId, { + au1: {componentAuctions: [fledgeAuctionConfig]}, + au2: {componentAuctions: [fledgeAuctionConfig]}, + au3: null + }) + }); + }); + it('removes configs from getPAAPIConfig if the module calls markAsUsed', () => { + submods[0].onAuctionConfig.callsFake((auctionId, configs, markAsUsed) => { + markAsUsed('au1'); + }); + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); + expect(getPAAPIConfig()).to.eql({}); + }); + it('keeps them available if they do not', () => { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']}); + expect(getPAAPIConfig()).to.not.be.empty; + }) + }); + }); + + describe('floor signal', () => { + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }); + + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { + if (from === to) return amount; + if (from === 'USD' && to === 'JPY') return amount * 100; + if (from === 'JPY' && to === 'USD') return amount / 100; + throw new Error('unexpected currency conversion'); + }); + }); + + Object.entries({ + 'bids': (payload, values) => { + payload.bidsReceived = values + .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) + .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]); + }, + 'no bids': (payload, values) => { + payload.bidderRequests = values + .map((val) => ({ + bids: [{ + adUnitCode: 'au', + getFloor: () => ({floor: val.amount, currency: val.cur}) + }] + })) + .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]); + } + }).forEach(([tcase, setup]) => { + describe(`when auction has ${tcase}`, () => { + Object.entries({ + 'no currencies': { + values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], + 'bids': { + bidfloor: 100, + bidfloorcur: undefined + }, + 'no bids': { + bidfloor: 1, + bidfloorcur: undefined, + } + }, + 'only zero values': { + values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], + 'bids': { + bidfloor: undefined, + bidfloorcur: undefined, + }, + 'no bids': { + bidfloor: undefined, + bidfloorcur: undefined, + } + }, + 'matching currencies': { + values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], + 'bids': { + bidfloor: 100, + bidfloorcur: 'JPY', + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + }, + 'mixed currencies': { + values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], + 'bids': { + bidfloor: 10, + bidfloorcur: 'USD' + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + } + }).forEach(([t, testConfig]) => { + const values = testConfig.values; + const {bidfloor, bidfloorcur} = testConfig[tcase]; + + describe(`with ${t}`, () => { + let payload; + beforeEach(() => { + payload = {auctionId}; + setup(payload, values); + }); + + it('should populate bidfloor/bidfloorcur', () => { + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, payload); + const signals = getPAAPIConfig({auctionId}).au.componentAuctions[0].auctionSignals; + expect(signals.prebid?.bidfloor).to.eql(bidfloor); + expect(signals.prebid?.bidfloorcur).to.eql(bidfloorcur); + }); + }); + }); + }); + }); + }); + }); + + describe('with multiple auctions', () => { + const AUCTION1 = 'auction1'; + const AUCTION2 = 'auction2'; + + function mockAuction(auctionId) { + return { + getAuctionId() { + return auctionId; + } + }; + } + + function expectAdUnitsFromAuctions(actualConfig, auToAuctionMap) { + expect(Object.keys(actualConfig)).to.have.members(Object.keys(auToAuctionMap)); + Object.entries(actualConfig).forEach(([au, cfg]) => { + cfg.componentAuctions.forEach(cmp => expect(cmp.auctionId).to.eql(auToAuctionMap[au])); + }); + } + + let configs; + beforeEach(() => { + const mockAuctions = [mockAuction(AUCTION1), mockAuction(AUCTION2)]; + sandbox.stub(auctionManager, 'index').value(new AuctionIndex(() => mockAuctions)); + configs = {[AUCTION1]: {}, [AUCTION2]: {}}; + Object.entries({ + [AUCTION1]: [['au1', 'au2'], ['missing-1']], + [AUCTION2]: [['au2', 'au3'], []], + }).forEach(([auctionId, [adUnitCodes, noConfigAdUnitCodes]]) => { + adUnitCodes.forEach(adUnitCode => { + const cfg = {...fledgeAuctionConfig, auctionId, adUnitCode}; + configs[auctionId][adUnitCode] = cfg; + addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode}, cfg); + }); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId, adUnitCodes: adUnitCodes.concat(noConfigAdUnitCodes)}); + }); + }); + + it('should filter by auction', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2}), {au2: AUCTION2, au3: AUCTION2}); + }); + + it('should filter by auction and ad unit', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1, adUnitCode: 'au2'}), {au2: AUCTION1}); + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION2, adUnitCode: 'au2'}), {au2: AUCTION2}); + }); + + it('should use last auction for each ad unit', () => { + expectAdUnitsFromAuctions(getPAAPIConfig(), {au1: AUCTION1, au2: AUCTION2, au3: AUCTION2}); + }); + + it('should filter by ad unit and use latest auction', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({adUnitCode: 'au2'}), {au2: AUCTION2}); + }); + + it('should keep track of which configs were returned', () => { + expectAdUnitsFromAuctions(getPAAPIConfig({auctionId: AUCTION1}), {au1: AUCTION1, au2: AUCTION1}); + expect(getPAAPIConfig({auctionId: AUCTION1})).to.eql({}); + expectAdUnitsFromAuctions(getPAAPIConfig(), {au2: AUCTION2, au3: AUCTION2}); + }); + + describe('includeBlanks = true', () => { + Object.entries({ + 'auction with blanks': { + filters: {auctionId: AUCTION1}, + expected: {au1: true, au2: true, 'missing-1': false} + }, + 'blank adUnit in an auction': { + filters: {auctionId: AUCTION1, adUnitCode: 'missing-1'}, + expected: {'missing-1': false} + }, + 'non-existing auction': { + filters: {auctionId: 'other'}, + expected: {} + }, + 'non-existing adUnit in an auction': { + filters: {auctionId: AUCTION2, adUnitCode: 'other'}, + expected: {} + }, + 'non-existing ad unit': { + filters: {adUnitCode: 'other'}, + expected: {}, + }, + 'non existing ad unit in a non-existing auction': { + filters: {adUnitCode: 'other', auctionId: 'other'}, + expected: {} + }, + 'all ad units': { + filters: {}, + expected: {'au1': true, 'au2': true, 'missing-1': false, 'au3': true} + } + }).forEach(([t, {filters, expected}]) => { + it(t, () => { + const cfg = getPAAPIConfig(filters, true); + expect(Object.keys(cfg)).to.have.members(Object.keys(expected)); + Object.entries(expected).forEach(([au, shouldBeFilled]) => { + if (shouldBeFilled) { + expect(cfg[au]).to.not.be.null; + } else { + expect(cfg[au]).to.be.null; + } + }) + }) + }) + }); + }); + }); + + describe('markForFledge', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { + navigator[p] = sinon.stub(); + }); + hook.ready(); + config.resetConfig(); + }); + + after(function () { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }); + + afterEach(function () { + config.resetConfig(); + }); + + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + + function expectFledgeFlags(...enableFlags) { + const bidRequests = Object.fromEntries( + adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() { + }, + [] + ).map(b => [b.bidderCode, b]) + ); + + expect(bidRequests.appnexus.fledgeEnabled).to.eql(enableFlags[0].enabled); + bidRequests.appnexus.bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)); + + expect(bidRequests.rubicon.fledgeEnabled).to.eql(enableFlags[1].enabled); + bidRequests.rubicon.bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); + } + + describe('with setBidderConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + defaultForSlots: 1, + fledgeEnabled: true + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + }); + }); + + describe('with setConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + [configNS]: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: false, ae: undefined}); + }); + + it('should set fledgeEnabled correctly for all bidders', function () { + config.setConfig({ + bidderSequence: 'fixed', + [configNS]: { + enabled: true, + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + }); + + it('should not override pub-defined ext.ae', () => { + config.setConfig({ + bidderSequence: 'fixed', + [configNS]: { + enabled: true, + defaultForSlots: 1, + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); + expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); + }); + }); + }); + }); + }); + + describe('ortb processors for fledge', () => { + it('imp.ext.ae should be removed if fledge is not enabled', () => { + const imp = {ext: {ae: 1}}; + setImpExtAe(imp, {}, {bidderRequest: {}}); + expect(imp.ext.ae).to.not.exist; + }); + it('imp.ext.ae should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: 2}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); + expect(imp.ext.ae).to.equal(2); + }); + describe('parseExtPrebidFledge', () => { + function packageConfigs(configs) { + return { + ext: { + prebid: { + fledge: { + auctionconfigs: configs + } + } + } + }; + } + + function generateImpCtx(fledgeFlags) { + return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + } + + function generateCfg(impid, ...ids) { + return ids.map((id) => ({impid, config: {id}})); + } + + function extractResult(ctx) { + return Object.fromEntries( + Object.entries(ctx) + .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) + .filter(([_, val]) => val != null) + ); + } + + it('should collect fledge configs by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = packageConfigs( + generateCfg('e1', 1, 2, 3) + .concat(generateCfg('e2', 4) + .concat(generateCfg('d1', 5, 6))) + ); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({ + e1: [1, 2, 3], + e2: [4], + }); + }); + it('should not choke if fledge config references unknown imp', () => { + const ctx = {impContext: generateImpCtx({i: 1})}; + const resp = packageConfigs(generateCfg('unknown', 1)); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({}); + }); + }); + describe('setResponseFledgeConfigs', () => { + it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { + const ctx = { + impContext: { + 1: { + bidRequest: {bidId: 'bid1'}, + fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] + }, + 2: { + bidRequest: {bidId: 'bid2'}, + fledgeConfigs: [{config: {id: 3}}] + }, + 3: { + bidRequest: {bidId: 'bid3'} + } + } + }; + const resp = {}; + setResponseFledgeConfigs(resp, {}, ctx); + expect(resp.fledgeAuctionConfigs).to.eql([ + {bidId: 'bid1', config: {id: 1}}, + {bidId: 'bid1', config: {id: 2}}, + {bidId: 'bid2', config: {id: 3}}, + ]); + }); + it('should not set fledgeAuctionConfigs if none exist', () => { + const resp = {}; + setResponseFledgeConfigs(resp, {}, { + impContext: { + 1: { + fledgeConfigs: [] + }, + 2: {} + } + }); + expect(resp).to.eql({}); + }); + }); + }); +}); diff --git a/test/spec/modules/pangleBidAdapter_spec.js b/test/spec/modules/pangleBidAdapter_spec.js new file mode 100644 index 00000000000..f2504a810c4 --- /dev/null +++ b/test/spec/modules/pangleBidAdapter_spec.js @@ -0,0 +1,391 @@ +import { expect } from 'chai'; +import { spec } from 'modules/pangleBidAdapter.js'; + +const REQUEST = [{ + adUnitCode: 'adUnitCode1', + bidId: 'bidId1', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}, +{ + adUnitCode: 'adUnitCode2', + bidId: 'bidId2', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}]; + +const DEFAULT_OPTIONS = { + userId: { + britepoolid: 'pangle-britepool', + criteoId: 'pangle-criteo', + digitrustid: { data: { id: 'pangle-digitrust' } }, + id5id: { uid: 'pangle-id5' }, + idl_env: 'pangle-idl-env', + lipb: { lipbid: 'pangle-liveintent' }, + netId: 'pangle-netid', + parrableId: { eid: 'pangle-parrable' }, + pubcid: 'pangle-pubcid', + tdid: 'pangle-ttd', + } +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'requestId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 1, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'pangle': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'seat' + } + ] + } +}; + +describe('pangle bid adapter', function () { + describe('isBidRequestValid', function () { + it('should accept request if placementid and appid is passed', function () { + let bid = { + bidder: 'pangle', + params: { + token: 'xxx', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('reject requests without params', function () { + let bid = { + bidder: 'pangle', + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('creates request data', function () { + let request1 = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + expect(request1).to.exist.and.to.be.a('object'); + const payload1 = request1.data; + expect(payload1.imp[0]).to.have.property('id', REQUEST[0].bidId); + + let request2 = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[1]; + expect(request2).to.exist.and.to.be.a('object'); + const payload2 = request2.data; + expect(payload2.imp[0]).to.have.property('id', REQUEST[1].bidId); + }); + }); + + describe('interpretResponse', function () { + it('has bids', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + let bids = spec.interpretResponse(RESPONSE, request); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].id); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + const EMPTY_RESP = Object.assign({}, RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); + }); + + describe('parseUserAgent', function () { + let desktop, mobile, tablet; + beforeEach(function () { + desktop = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'; + mobile = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'; + tablet = 'Apple iPad: Mozilla/5.0 (iPad; CPU OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/605.1.15'; + }); + + it('should return correct device type: tablet', function () { + let deviceType = spec.getDeviceType(tablet); + expect(deviceType).to.equal(5); + }); + + it('should return correct device type: mobile', function () { + let deviceType = spec.getDeviceType(mobile); + expect(deviceType).to.equal(4); + }); + + it('should return correct device type: desktop', function () { + let deviceType = spec.getDeviceType(desktop); + expect(deviceType).to.equal(2); + }); + }); +}); + +describe('Pangle Adapter with video', function() { + const videoBidRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const serverResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 1, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + + describe('Video: buildRequests', function() { + it('should create a POST request for video bid', function() { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].method).to.equal('POST'); + }); + + it('should have a valid URL and payload for an out-stream video bid', function () { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].url).to.equal('https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'); + expect(requests[0].data).to.exist; + }); + }); + + describe('interpretResponse: Video', function () { + it('should get correct bid response', function () { + const request = spec.buildRequests(videoBidRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(serverResponse, request); + expect(interpretedResponse).to.be.an('array'); + const bid = interpretedResponse[0]; + expect(bid).to.exist; + expect(bid.requestId).to.exist; + expect(bid.cpm).to.be.above(0); + expect(bid.ttl).to.exist; + expect(bid.creativeId).to.exist; + if (bid.renderer) { + expect(bid.renderer.render).to.exist; + } + }); + }); +}); + +describe('pangle multi-format ads', function () { + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const multiRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { banner: { sizes: [[300, 250]] }, video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const videoResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 2, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + const bannerResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 1, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + it('should set mediaType to banner', function() { + const request = spec.buildRequests(multiRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(bannerResponse, request); + const bid = interpretedResponse[0]; + expect(bid.mediaType).to.equal('banner'); + }) + it('should set mediaType to video', function() { + const request = spec.buildRequests(multiRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(videoResponse, request); + const bid = interpretedResponse[0]; + expect(bid.mediaType).to.equal('video'); + }) +}); diff --git a/test/spec/modules/pgamsspBidAdapter_spec.js b/test/spec/modules/pgamsspBidAdapter_spec.js new file mode 100644 index 00000000000..0766219eda8 --- /dev/null +++ b/test/spec/modules/pgamsspBidAdapter_spec.js @@ -0,0 +1,400 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/pgamsspBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'pgamssp' + +describe('PGAMBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, 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://us-east.pgammedia.com/pbjs'); + }); + + it('Returns general data 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', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + 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.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.an('array'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, 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 = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, 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([], bidderRequest); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + 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', 'meta'); + 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'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.pgammedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.pgammedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index e4f06c8835f..2bab144dae7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -6,7 +6,7 @@ import { resetWurlMap, s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; -import adapterManager from 'src/adapterManager.js'; +import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; @@ -14,7 +14,6 @@ import {config} from 'src/config.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {server} from 'test/mocks/xhr.js'; -import {createEidsArray} from 'modules/userId/eids.js'; import 'modules/appnexusBidAdapter.js'; // appnexus alias test import 'modules/rubiconBidAdapter.js'; // rubicon alias test import 'src/prebid.js'; // $$PREBID_GLOBAL$$.aliasBidder test @@ -27,14 +26,17 @@ import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import 'modules/fledgeForGpt.js'; import * as redactor from 'src/activities/redactor.js'; +import * as activityRules from 'src/activities/rules.js'; import {hook} from '../../../src/hook.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; -import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; +import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; let CONFIG = { accountId: '1', @@ -89,6 +91,7 @@ const REQUEST = { } }, 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', + 'adUnitId': 'au-id-1', 'bids': [ { 'bid_id': '123', @@ -707,8 +710,8 @@ describe('S2S Adapter', function () { beforeEach(() => { s2sReq = { ...REQUEST, - ortb2Fragments: {global: {source: {tid: 'mock-tid'}}}, - ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})) + ortb2Fragments: {global: {}}, + ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})), }; BID_REQUESTS[0].bids[0].ortb2Imp = {ext: {tid: 'mock-tid'}}; }); @@ -721,26 +724,52 @@ describe('S2S Adapter', function () { it('should not be set when transmitTid is not allowed, with ext.prebid.createtids: false', () => { config.setConfig({ s2sConfig: CONFIG, enableTIDs: false }); const req = makeRequest(); - expect(req.source.tid).to.not.exist; - expect(req.imp[0].ext.tid).to.not.exist; + expect(req.source?.tid).to.not.exist; + expect(req.imp[0].ext?.tid).to.not.exist; expect(req.ext.prebid.createtids).to.equal(false); }); - it('should be picked from FPD otherwise', () => { + it('should be set to auction ID otherwise', () => { config.setConfig({s2sConfig: CONFIG, enableTIDs: true}); const req = makeRequest(); - expect(req.source.tid).to.eql('mock-tid'); + expect(req.source.tid).to.eql(BID_REQUESTS[0].auctionId); expect(req.imp[0].ext.tid).to.eql('mock-tid'); }) }) + describe('browsingTopics', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore() + }); + Object.entries({ + 'allowed': true, + 'not allowed': false, + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + sandbox.stub(activityRules, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_TRANSMIT_UFPD && params.component === `${MODULE_TYPE_PREBID}.${PBS_ADAPTER_NAME}`) { + return allow; + } + return false; + }); + config.setConfig({s2sConfig: CONFIG}); + const ajax = sinon.stub(); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + sinon.assert.calledWith(ajax, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: allow + })); + }); + }); + }) + it('should set tmax to s2sConfig.timeout', () => { const cfg = {...CONFIG, timeout: 123}; config.setConfig({s2sConfig: cfg}); adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); const req = JSON.parse(server.requests[0].requestBody); expect(req.tmax).to.eql(123); - }) + }); it('should block request if config did not define p1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); @@ -848,7 +877,7 @@ describe('S2S Adapter', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); - function mockConsent({applies = true, hasP1Consent = true} = {}) { + function mockTCF({applies = true, hasP1Consent = true} = {}) { return { consentString: 'mockConsent', gdprApplies: applies, @@ -866,7 +895,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent(); + gdprBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -889,7 +918,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = Object.assign(mockConsent(), { + gdprBidRequest[0].gdprConsent = Object.assign(mockTCF(), { addtlConsent: 'superduperconsent', }); @@ -909,69 +938,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; expect(requestBid.user).to.not.exist; }); - - it('check gdpr info gets added into cookie_sync request: have consent data', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - - gdprBidRequest[0].gdprConsent = mockConsent(); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('mockConsent'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); - - it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); - - it('checks gdpr info gets added to cookie_sync request: applies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); }); describe('us_privacy (ccpa) consent data', function () { @@ -998,25 +964,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; }); - - it('is added to cookie_sync request when in bidRequest', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let uspBidRequest = utils.deepClone(BID_REQUESTS); - uspBidRequest[0].uspConsent = '1YNN'; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, uspBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.us_privacy).is.equal('1YNN'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); }); describe('gdpr and us_privacy (ccpa) consent data', function () { @@ -1029,7 +976,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -1055,7 +1002,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1YNN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig @@ -1812,170 +1759,177 @@ describe('S2S Adapter', function () { }]); }); - describe('filterSettings', function () { - const getRequestBid = userSync => { - let cookieSyncConfig = utils.deepClone(CONFIG); - const s2sBidRequest = utils.deepClone(REQUEST); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - s2sBidRequest.s2sConfig = cookieSyncConfig; + describe('cookie sync', () => { + let s2sConfig, bidderReqs; - config.setConfig({ userSync, s2sConfig: cookieSyncConfig }); + beforeEach(() => { + bidderReqs = utils.deepClone(BID_REQUESTS); + s2sConfig = utils.deepClone(CONFIG); + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + }) - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + function callCookieSync() { + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig({ s2sConfig: s2sConfig }); + adapter.callBids(s2sBidRequest, bidderReqs, addBidResponse, done, ajax); return JSON.parse(server.requests[0].requestBody); } - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['appnexus', 'rubicon', 'pubmatic'], - filter: 'exclude' + describe('filterSettings', function () { + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['appnexus', 'rubicon', 'pubmatic'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); - - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - }, - 'iframe': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - } + }); + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + }, + 'iframe': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - iframe: { - bidders: ['rubicon', 'pubmatic'], - filter: 'include' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['rubicon', 'pubmatic'], + filter: 'include' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': '*', - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['rubicon', 'pubmatic'], - 'filter': 'include' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': '*', + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['rubicon', 'pubmatic'], + 'filter': 'include' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - image: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); }); - }); - - it('adds limit to the cookie_sync request if userSyncLimit is greater than 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - cookieSyncConfig.userSyncLimit = 1; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.equal(1); - }); - it('does not add limit to cooke_sync request if userSyncLimit is missing or 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); + describe('limit', () => { + it('is added to request if userSyncLimit is greater than 0', function () { + s2sConfig.userSyncLimit = 1; + const req = callCookieSync(); + expect(req.limit).is.equal(1); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + Object.entries({ + 'missing': () => null, + '0': () => { s2sConfig.userSyncLimit = 0; } + }).forEach(([t, setup]) => { + it(`is not added to request if userSyncLimit is ${t}`, () => { + setup(); + const req = callCookieSync(); + expect(req.limit).to.not.exist; + }); + }); + }); - cookieSyncConfig.userSyncLimit = 0; - config.resetConfig(); - config.setConfig({ s2sConfig: cookieSyncConfig }); + describe('gdpr data is set', () => { + it('when we have consent data', function () { + bidderReqs[0].gdprConsent = mockTCF(); + const req = callCookieSync(); + expect(req.gdpr).is.equal(1); + expect(req.gdpr_consent).is.equal('mockConsent'); + }); - const s2sBidRequest2 = utils.deepClone(REQUEST); - s2sBidRequest2.s2sConfig = cookieSyncConfig; + it('when gdprApplies is false', () => { + bidderReqs[0].gdprConsent = mockTCF({applies: false}); + const req = callCookieSync(); + expect(req.gdpr).is.equal(0); + expect(req.gdpr_consent).is.undefined; + }); + }); - bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest2, bidRequest, addBidResponse, done, ajax); - requestBid = JSON.parse(server.requests[0].requestBody); + it('adds USP data from bidder request', () => { + bidderReqs[0].uspConsent = '1YNN'; + expect(callCookieSync().us_privacy).to.equal('1YNN'); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + it('adds GPP data from bidder requests', () => { + bidderReqs[0].gppConsent = { + applicableSections: [1, 2, 3], + gppString: 'mock-string' + }; + const req = callCookieSync(); + expect(req.gpp).to.eql('mock-string'); + expect(req.gpp_sid).to.eql('1,2,3'); + }); }); it('adds s2sConfig adapterOptions to request for ORTB', function () { @@ -2098,7 +2052,7 @@ describe('S2S Adapter', function () { const bidRequests = utils.deepClone(BID_REQUESTS); adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); - const parsedRequestBody = JSON.parse(server.requests[1].requestBody); + const parsedRequestBody = JSON.parse(server.requests.find(req => req.method === 'POST').requestBody); expect(parsedRequestBody.cur).to.deep.equal(['NZ']); }); @@ -2920,6 +2874,41 @@ describe('S2S Adapter', function () { events.emit.restore(); }); + it('triggers BIDDER_ERROR on server error', () => { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(400, {}, {}); + BID_REQUESTS.forEach(bidderRequest => { + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.BIDDER_ERROR, sinon.match({bidderRequest})) + }) + }) + + describe('calls done', () => { + let success, error; + beforeEach(() => { + const mockAjax = function (_, callback) { + ({success, error} = callback); + } + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, mockAjax); + }) + + it('passing timedOut = false on succcess', () => { + success({}); + sinon.assert.calledWith(done, false); + }); + + Object.entries({ + 'timeouts': true, + 'other errors': false + }).forEach(([t, timedOut]) => { + it(`passing timedOut = ${timedOut} on ${t}`, () => { + error('', {timedOut}); + sinon.assert.calledWith(done, timedOut); + }) + }) + }) + // TODO: test dependent on pbjs_api_spec. Needs to be isolated it('does not call addBidResponse and calls done when ad unit not set', function () { config.setConfig({ s2sConfig: CONFIG }); @@ -3460,29 +3449,6 @@ describe('S2S Adapter', function () { }); }); describe('when the response contains ext.prebid.fledge', () => { - let fledgeStub, request, bidderRequests; - - function fledgeHook(next, ...args) { - fledgeStub(...args); - } - - before(() => { - addComponentAuction.before(fledgeHook); - }); - - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) - - beforeEach(function () { - fledgeStub = sinon.stub(); - config.setConfig({CONFIG}); - request = deepClone(REQUEST); - request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); - bidderRequests = deepClone(BID_REQUESTS); - bidderRequests.forEach(req => req.fledgeEnabled = true); - }); - const AU = 'div-gpt-ad-1460505748561-0'; const FLEDGE_RESP = { ext: { @@ -3491,12 +3457,14 @@ describe('S2S Adapter', function () { auctionconfigs: [ { impid: AU, + bidder: 'appnexus', config: { id: 1 } }, { impid: AU, + bidder: 'other', config: { id: 2 } @@ -3507,20 +3475,62 @@ describe('S2S Adapter', function () { } } + let fledgeStub, request, bidderRequests; + + function fledgeHook(next, ...args) { + fledgeStub(...args); + } + + before(() => { + addComponentAuction.before(fledgeHook); + }); + + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) + + beforeEach(function () { + fledgeStub = sinon.stub(); + config.setConfig({CONFIG}); + bidderRequests = deepClone(BID_REQUESTS); + AU + bidderRequests.forEach(req => { + Object.assign(req, { + fledgeEnabled: true, + ortb2: { + fpd: 1 + } + }) + req.bids.forEach(bid => { + Object.assign(bid, { + ortb2Imp: { + fpd: 2 + } + }) + }) + }); + request = deepClone(REQUEST); + request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); + }); + + function expectFledgeCalls() { + const auctionId = bidderRequests[0].auctionId; + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: bidderRequests[0].ortb2, ortb2Imp: bidderRequests[0].bids[0].ortb2Imp}), {id: 1}) + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), {id: 2}) + } + it('calls addComponentAuction alongside addBidResponse', function () { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); expect(addBidResponse.called).to.be.true; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }); it('calls addComponentAuction when there is no bid in the response', () => { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); expect(addBidResponse.called).to.be.false; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }) }); }); @@ -3670,33 +3680,6 @@ describe('S2S Adapter', function () { sinon.assert.calledOnce(logErrorSpy); }); - it('should configure the s2sConfig object with appnexus vendor defaults unless specified by user', function () { - const options = { - accountId: '123', - bidders: ['appnexus'], - defaultVendor: 'appnexus', - timeout: 750 - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', '123'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['appnexus']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' - }); - expect(vendorConfig.syncEndpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' - }); - expect(vendorConfig).to.have.property('timeout', 750); - }); - it('should configure the s2sConfig object with appnexuspsp vendor defaults unless specified by user', function () { const options = { accountId: '123', @@ -3717,7 +3700,10 @@ describe('S2S Adapter', function () { p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }); - expect(vendorConfig.syncEndpoint).to.be.undefined; + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -3802,6 +3788,74 @@ describe('S2S Adapter', function () { }) }); + it('should configure the s2sConfig object with openwrap vendor defaults unless specified by user', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap' + }; + + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); + + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', '1234'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['pubmatic']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }); + expect(vendorConfig).to.have.property('timeout', 500); + }); + + it('should return proper defaults', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + }; + + config.setConfig({ s2sConfig: options }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + 'accountId': '1234', + 'adapter': 'prebidServer', + 'bidders': ['pubmatic'], + 'defaultVendor': 'openwrap', + 'enabled': true, + 'endpoint': { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + 'timeout': 500 + }) + }); + + it('should return default adapterOptions if not set', function () { + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + enabled: true, + timeout: 500, + adapter: 'prebidServer', + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + }) + }); + it('should set adapterOptions', function () { config.setConfig({ s2sConfig: { diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js new file mode 100644 index 00000000000..78a1615a02e --- /dev/null +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -0,0 +1,162 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/precisoBidAdapter.js'; +import { config } from '../../../src/config.js'; + +const DEFAULT_PRICE = 1 +const DEFAULT_CURRENCY = 'USD' +const DEFAULT_BANNER_WIDTH = 300 +const DEFAULT_BANNER_HEIGHT = 250 +const BIDDER_CODE = 'preciso'; + +describe('PrecisoAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'preciso', + mediaTypes: { + banner: { + sizes: [[DEFAULT_BANNER_WIDTH, DEFAULT_BANNER_HEIGHT]] + } + }, + params: { + host: 'prebid', + sourceid: '0', + publisherId: '0', + mediaType: 'banner', + region: 'prebid-eu' + + }, + userId: { + pubcid: '12355454test' + + }, + geo: 'NA', + city: 'Asia,delhi' + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and sourceid 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.publisherId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + 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://ssp-bidder.mndtrk.com/bid_request/openrtb'); + }); + 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('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); + + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.coppa).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.city).to.be.a('string'); + expect(data.geo).to.be.a('object'); + // expect(data.userId).to.be.a('string'); + // expect(data.imp).to.be.a('object'); + }); + // it('Returns empty data if no valid requests are passed', function () { + /// serverRequest = spec.buildRequests([]); + // let data = serverRequest.data; + // expect(data.imp).to.be.an('array').that.is.empty; + // }); + }); + + describe('with COPPA', function () { + beforeEach(function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + }); + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + let serverRequest = spec.buildRequests([bid]); + expect(serverRequest.data.coppa).to.equal(1); + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid response', function () { + let response = { + + bidderRequestId: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + price: DEFAULT_PRICE, + adm: 'hi', + cid: 'test_cid', + crid: 'test_banner_crid', + w: DEFAULT_BANNER_WIDTH, + h: DEFAULT_BANNER_HEIGHT, + adomain: [], + } + ], + seat: BIDDER_CODE + } + ], + } + + let expectedResponse = [ + { + requestId: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + cpm: DEFAULT_PRICE, + width: DEFAULT_BANNER_WIDTH, + height: DEFAULT_BANNER_HEIGHT, + creativeId: 'test_banner_crid', + ad: 'hi', + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: 300, + meta: { advertiserDomains: [] }, + } + ] + let result = spec.interpretResponse({ body: response }) + + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])) + }) + }) + describe('getUserSyncs', function () { + const syncUrl = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=NA&gdpr=0&gdpr_consent=&us_privacy=&t=4'; + const syncOptions = { + iframeEnabled: true + }; + let userSync = spec.getUserSyncs(syncOptions); + it('Returns valid URL and type', function () { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync).to.deep.equal([ + { type: 'iframe', url: syncUrl } + ]); + }); + }); +}); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index e9ce4c8ccd3..7ea7722b12a 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -12,7 +12,7 @@ import { isFloorsDataValid, addBidResponseHook, fieldMatchingFunctions, - allowedFields + allowedFields, parseFloorData, normalizeDefault, getFloorDataFromAdUnits, updateAdUnitsForAuction, createFloorsDataForAuction } from 'modules/priceFloors.js'; import * as events from 'src/events.js'; import * as mockGpt from '../integration/faker/googletag.js'; @@ -20,6 +20,9 @@ import 'src/prebid.js'; import {createBid} from '../../../src/bidfactory.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {guardTids} from '../../../src/adapters/bidderFactory.js'; +import * as activities from '../../../src/activities/rules.js'; +import {server} from '../../mocks/xhr.js'; describe('the price floors module', function () { let logErrorSpy; @@ -113,14 +116,15 @@ describe('the price floors module', function () { bidder: 'rubicon', adUnitCode: 'test_div_1', auctionId: '1234-56-789', - transactionId: 'tr_test_div_1' + transactionId: 'tr_test_div_1', + adUnitId: 'tr_test_div_1', }; function getAdUnitMock(code = 'adUnit-code') { return { code, mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}}, - bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}] + bids: [{bidder: 'someBidder', adUnitCode: code}, {bidder: 'someOtherBidder', adUnitCode: code}] }; } beforeEach(function() { @@ -140,6 +144,76 @@ describe('the price floors module', function () { getGlobal().bidderSettings = {}; }); + describe('parseFloorData', () => { + it('should accept just a default floor', () => { + const fd = parseFloorData({ + default: 1.23 + }); + expect(getFirstMatchingFloor(fd, {}, {}).matchingFloor).to.eql(1.23); + }); + }); + + describe('getFloorDataFromAdUnits', () => { + let adUnits; + + function setFloorValues(rule) { + adUnits.forEach((au, i) => { + au.floors = { + values: { + [rule]: i + 1 + } + } + }) + } + + beforeEach(() => { + adUnits = ['au1', 'au2', 'au3'].map(getAdUnitMock); + }) + + it('should use one schema for all adUnits', () => { + setFloorValues('*;*') + adUnits[1].floors.schema = { + fields: ['mediaType', 'gptSlot'], + delimiter: ';' + } + sinon.assert.match(getFloorDataFromAdUnits(adUnits), { + schema: { + fields: ['adUnitCode', 'mediaType', 'gptSlot'], + delimiter: ';' + }, + values: { + 'au1;*;*': 1, + 'au2;*;*': 2, + 'au3;*;*': 3 + } + }) + }); + it('should ignore adUnits that declare different schema', () => { + setFloorValues('*|*'); + adUnits[0].floors.schema = { + fields: ['mediaType', 'gptSlot'] + }; + adUnits[2].floors.schema = { + fields: ['gptSlot', 'mediaType'] + }; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*|*': 1, + 'au2|*|*': 2 + }) + }); + it('should ignore adUnits that declare no values', () => { + setFloorValues('*'); + adUnits[0].floors.schema = { + fields: ['mediaType'] + }; + delete adUnits[2].floors.values; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*': 1, + 'au2|*': 2, + }) + }) + }) + describe('getFloorsDataForAuction', function () { it('converts basic input floor data into a floorData map for the auction correctly', function () { // basic input where nothing needs to be updated @@ -230,8 +304,8 @@ describe('the price floors module', function () { }); describe('getFirstMatchingFloor', function () { - it('uses a 0 floor as overrite', function () { - let inputFloorData = { + it('uses a 0 floor as override', function () { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '|', @@ -242,7 +316,7 @@ describe('the price floors module', function () { 'test_div_2': 2 }, default: 0.5 - }; + }); expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ floorMin: 0, @@ -431,7 +505,7 @@ describe('the price floors module', function () { }); }); it('selects the right floor for more complex rules', function () { - let inputFloorData = { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '^', @@ -445,7 +519,7 @@ describe('the price floors module', function () { 'weird_div^*^300x250': 5.5 }, default: 0.5 - }; + }); // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ floorMin: 0, @@ -487,10 +561,8 @@ describe('the price floors module', function () { matchingFloor: undefined }); // if default is there use it - inputFloorData = { default: 5.0 }; - expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ - matchingFloor: 5.0 - }); + inputFloorData = normalizeDefault({ default: 5.0 }); + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'}).matchingFloor).to.equal(5.0); }); describe('with gpt enabled', function () { let gptFloorData; @@ -547,8 +619,8 @@ describe('the price floors module', function () { }); }); it('picks the gptSlot from the adUnit and does not call the slotMatching', function () { - const newBidRequest1 = { ...basicBidRequest, transactionId: 'au1' }; - adUnits = [{code: newBidRequest1.code, transactionId: 'au1'}]; + const newBidRequest1 = { ...basicBidRequest, adUnitId: 'au1' }; + adUnits = [{code: newBidRequest1.adUnitCode, adUnitId: 'au1'}]; utils.deepSetValue(adUnits[0], 'ortb2Imp.ext.data.adserver', { name: 'gam', adslot: '/12345/news/politics' @@ -561,8 +633,8 @@ describe('the price floors module', function () { matchingRule: '/12345/news/politics' }); - const newBidRequest2 = { ...basicBidRequest, adUnitCode: 'test_div_2', transactionId: 'au2' }; - adUnits = [{code: newBidRequest2.adUnitCode, transactionId: newBidRequest2.transactionId}]; + const newBidRequest2 = { ...basicBidRequest, adUnitCode: 'test_div_2', adUnitId: 'au2' }; + adUnits = [{code: newBidRequest2.adUnitCode, adUnitId: newBidRequest2.adUnitId}]; utils.deepSetValue(adUnits[0], 'ortb2Imp.ext.data.adserver', { name: 'gam', adslot: '/12345/news/weather' @@ -577,6 +649,107 @@ describe('the price floors module', function () { }); }); }); + + describe('updateAdUnitsForAuction', function() { + let inputFloorData; + let adUnits; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + inputFloorData = utils.deepClone(minFloorConfigLow); + inputFloorData.skipRate = 0.5; + }); + + it('should set the skipRate to the skipRate from the data property before using the skipRate from floorData directly', function() { + utils.deepSetValue(inputFloorData, 'data', { + skipRate: 0.7 + }); + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(0.7); + }); + + it('should set the skipRate to the skipRate from floorData directly if it does not exist in the data property of floorData', function() { + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(0.5); + }); + + it('should set the skipRate in the bid floorData to undefined if both skipRate and skipRate in the data property are undefined', function() { + inputFloorData.skipRate = undefined; + utils.deepSetValue(inputFloorData, 'data', { + skipRate: undefined, + }); + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(undefined); + }); + }); + + describe('createFloorsDataForAuction', function() { + let adUnits; + let floorConfig; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + floorConfig = utils.deepClone(basicFloorConfig); + }); + + it('should return skipRate as 0 if both skipRate and skipRate in the data property are undefined', function() { + floorConfig.skipRate = undefined; + floorConfig.data.skipRate = undefined; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.skipRate).to.equal(0); + expect(floorData.skipped).to.equal(false); + }); + + it('should properly set skipRate if it is available in the data property', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + floorConfig.data.skipRate = 201; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.data.skipRate).to.equal(201); + expect(floorData.skipped).to.equal(true); + }); + + it('should should use the skipRate if its not available in the data property ', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.skipRate).to.equal(101); + expect(floorData.skipped).to.equal(true); + }); + + it('should have skippedReason set to "not_found" if there is no valid floor data', function() { + floorConfig.data = {} + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + expect(floorData.skippedReason).to.equal(CONSTANTS.FLOOR_SKIPPED_REASON.NOT_FOUND); + }); + + it('should have skippedReason set to "random" if there is floor data and skipped is true', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + expect(floorData.skippedReason).to.equal(CONSTANTS.FLOOR_SKIPPED_REASON.RANDOM); + }); + }); + describe('pre-auction tests', function () { let exposedAdUnits; const validateBidRequests = (getFloorExpected, FloorDataExpected) => { @@ -591,16 +764,11 @@ describe('the price floors module', function () { adUnits, }); }; - let fakeFloorProvider; let actualAllowedFields = allowedFields; let actualFieldMatchingFunctions = fieldMatchingFunctions; const defaultAllowedFields = [...allowedFields]; const defaultMatchingFunctions = {...fieldMatchingFunctions}; - beforeEach(function() { - fakeFloorProvider = sinon.fakeServer.create(); - }); afterEach(function() { - fakeFloorProvider.restore(); exposedAdUnits = undefined; actualAllowedFields = [...defaultAllowedFields]; actualFieldMatchingFunctions = {...defaultMatchingFunctions}; @@ -623,6 +791,124 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + it('should not do floor stuff if floors.data is defined by noFloorSignalBidders[]', function() { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + }}); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('should not do floor stuff if floors.enforcement is defined by noFloorSignalBidders[]', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + }, + data: basicFloorDataLow + }); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('should not do floor stuff and use first floors.data.noFloorSignalBidders if its defined betwen enforcement.noFloorSignalBidders', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + noFloorSignalBidders: ['someBidder'] + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + } + }); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('it shouldn`t return floor stuff for bidder in the noFloorSignalBidders list', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder'] + } + }); + runStandardAuction() + const bidRequestData = exposedAdUnits[0].bids.find(bid => bid.bidder === 'someBidder'); + expect(bidRequestData.hasOwnProperty('getFloor')).to.equal(false); + sinon.assert.match(bidRequestData.floorData, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }); + }) + it('it should return floor stuff if we defined wrong bidder name in data.noFloorSignalBidders', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['randomBiider'] + } + }); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: false + }) + }); it('should use adUnit level data if not setConfig or fetch has occured', function () { handleSetFloorsConfig({ ...basicFloorConfig, @@ -695,6 +981,95 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + describe('default floor', () => { + let adUnits; + beforeEach(() => { + adUnits = ['au1', 'au2'].map(getAdUnitMock); + }) + function expectFloors(floors) { + runStandardAuction(adUnits); + adUnits.forEach((au, i) => { + au.bids.forEach(bid => { + expect(bid.getFloor().floor).to.eql(floors[i]); + }) + }) + } + describe('should be sufficient by itself', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + default: 1.23 + } + }); + expectFloors([1.23, 1.23]) + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = {default: 1}; + adUnits[1].floors = {default: 2}; + expectFloors([1, 2]) + }); + it('on an adUnit with hidden schema', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + default: 1 + } + adUnits[1].floors = { + default: 2 + } + expectFloors([1, 2]); + }) + }); + describe('should NOT be used when a star rule exists', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 2 + }, + default: 3, + } + }); + expectFloors([2, 2]); + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 1 + }, + default: 3 + }; + adUnits[1].floors = { + values: { + '*|*': 2 + }, + default: 4 + } + expectFloors([1, 2]); + }) + }); + }) it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'}); runStandardAuction(); @@ -984,7 +1359,7 @@ describe('the price floors module', function () { }); }); it('Should continue auction of delay is hit without a response from floor provider', function () { - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json//'}}); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1011,7 +1386,7 @@ describe('the price floors module', function () { fetchStatus: 'timeout', floorProvider: undefined }); - fakeFloorProvider.respond(); + server.respond(); }); it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { // init the fake server with response stuff @@ -1019,14 +1394,14 @@ describe('the price floors module', function () { ...basicFloorData, modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1035,7 +1410,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1059,14 +1434,14 @@ describe('the price floors module', function () { floorProvider: 'floorProviderD', // change the floor provider modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1075,7 +1450,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); // the exposedAdUnits should be from the fetch not setConfig level data // and fetchStatus is success since fetch worked @@ -1100,14 +1475,14 @@ describe('the price floors module', function () { modelVersion: 'fetch model name', // change the model name }; fetchFloorData.skipRate = 95; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1116,7 +1491,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1135,10 +1510,10 @@ describe('the price floors module', function () { }); it('Should not break if floor provider returns 404', function () { // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond with 404 - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for fetch error @@ -1158,13 +1533,13 @@ describe('the price floors module', function () { }); }); it('Should not break if floor provider returns non json', function () { - fakeFloorProvider.respondWith('Not valid response'); + server.respondWith('Not valid response'); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for response floor data not being valid @@ -1185,27 +1560,27 @@ describe('the price floors module', function () { }); it('should handle not using fetch correctly', function () { // run setConfig twice indicating fetch - fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + server.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // log warn should be called and server only should have one request expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // now we respond and then run again it should work and make another request - fakeFloorProvider.respond(); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - fakeFloorProvider.respond(); + server.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + server.respond(); // now warn still only called once and server called twice expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(2); + expect(server.requests.length).to.equal(2); // should log error if method is not GET for now expect(logErrorSpy.calledOnce).to.equal(false); - handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakefloorprovider.json/', method: 'POST'}}); expect(logErrorSpy.calledOnce).to.equal(true); }); describe('isFloorsDataValid', function () { @@ -1369,10 +1744,22 @@ describe('the price floors module', function () { floor: 2.5 }); }); + + it('works when TIDs are disabled', () => { + sandbox.stub(activities, 'isActivityAllowed').returns(false); + const req = utils.deepClone(bidRequest); + _floorDataForAuction[req.auctionId] = utils.deepClone(basicFloorConfig); + + expect(guardTids('mock-bidder').bidRequest(req).getFloor({})).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + }); + it('picks the right rule with more complex rules', function () { _floorDataForAuction[bidRequest.auctionId] = { ...basicFloorConfig, - data: { + data: normalizeDefault({ currency: 'USD', schema: { fields: ['mediaType', 'size'], delimiter: '|' }, values: { @@ -1384,7 +1771,7 @@ describe('the price floors module', function () { 'video|*': 5.5 }, default: 10.0 - } + }) }; // assumes banner * @@ -1844,6 +2231,12 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); expect(logWarnSpy.calledOnce).to.equal(true); }); + it('if it finds a rule with a floor price of zero it should not call log warn', function () { + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { '*': 0 }; + runBidResponse(); + expect(logWarnSpy.calledOnce).to.equal(false); + }); it('if it finds a rule and floors should update the bid accordingly', function () { _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; @@ -1970,7 +2363,7 @@ describe('the price floors module', function () { } const resp = { - transactionId: req.transactionId, + adUnitId: req.adUnitId, size: [100, 100], mediaType: 'banner', } @@ -1981,7 +2374,7 @@ describe('the price floors module', function () { adUnits: [ { code: req.adUnitCode, - transactionId: req.transactionId, + adUnitId: req.adUnitId, ortb2Imp: {ext: {data: {adserver: {name: 'gam', adslot: 'slot'}}}} } ] diff --git a/test/spec/modules/programmaticaBidAdapter_spec.js b/test/spec/modules/programmaticaBidAdapter_spec.js new file mode 100644 index 00000000000..247d20752c3 --- /dev/null +++ b/test/spec/modules/programmaticaBidAdapter_spec.js @@ -0,0 +1,263 @@ +import { expect } from 'chai'; +import { spec } from 'modules/programmaticaBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('programmaticaBidAdapterTests', function () { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + } + }) + ).to.equal(true); + }); + + it('validate_generated_url', function () { + const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 }); + let req_url = request[0].url; + + expect(req_url).to.equal('https://asr.programmatica.com/get'); + }); + + it('validate_response_params', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': null, + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + 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(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }); + + it('validate_response_params_imps', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': [ + 'testImp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + 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(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }) + + it('validate_invalid_response', function () { + let serverResponse = { + body: {} + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(0); + }) + + it('video_bid', function () { + const bidRequest = deepClone(bidRequestData.bids); + bidRequest[0].mediaTypes = { + video: { + playerSize: [234, 765] + } + }; + + const request = spec.buildRequests(bidRequest, { timeout: 1234 }); + const vastXml = ''; + let serverResponse = { + body: { + 'id': 'cki2n3n6snkuulqutpf0', + 'type': { + 'format': '', + 'source': 'rtb', + 'dspId': '1' + }, + 'content': { + 'data': vastXml, + 'imps': [ + 'https://asr.dev.programmatica.com/track/imp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '', + 'matching': '', + 'cpm': 70, + 'currency': 'RUB' + } + }; + + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(vastXml); + expect(bid.width).to.equal(234); + expect(bid.height).to.equal(765); + }); +}); + +describe('getUserSyncs', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({ + pixelEnabled: true, + }, {}, {}, '1---'); + 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('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp?usp=1---&consent=') + }); + + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: 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('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1') + }); + + it('Should return array of objects with proper sync config , include GDPR, no purpose', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: false + }, + }, + } + }, ''); + expect(syncData).is.empty; + }); + + it('Should return array of objects with proper sync config , GDPR not applies', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: false, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + }, ''); + 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('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0') + }); +}) diff --git a/test/spec/modules/pstudioBidAdapter_spec.js b/test/spec/modules/pstudioBidAdapter_spec.js new file mode 100644 index 00000000000..52ecb820ed3 --- /dev/null +++ b/test/spec/modules/pstudioBidAdapter_spec.js @@ -0,0 +1,514 @@ +import { assert } from 'chai'; +import sinon from 'sinon'; +import { spec, storage } from 'modules/pstudioBidAdapter.js'; +import { deepClone } from '../../../src/utils.js'; + +describe('PStudioAdapter', function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + const bannerBid = { + bidder: 'pstudio', + params: { + pubid: '258c2a8d-d2ad-4c31-a2a5-e63001186456', + floorPrice: 1.15, + }, + adUnitCode: 'test-div-1', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + pos: 0, + name: 'some-name', + }, + }, + bidId: '30b31c1838de1e', + }; + + const videoBid = { + bidder: 'pstudio', + params: { + pubid: '258c2a8d-d2ad-4c31-a2a5-e63001186456', + floorPrice: 1.15, + }, + adUnitCode: 'test-div-1', + mediaTypes: { + video: { + playerSize: [[300, 250]], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + protocols: [2, 3], + startdelay: 5, + placement: 2, + skip: 1, + skipafter: 1, + minbitrate: 10, + maxbitrate: 10, + delivery: 1, + playbackmethod: [1, 3], + api: [2], + linearity: 1, + }, + }, + bidId: '30b31c1838de1e', + }; + + const bidWithOptionalParams = deepClone(bannerBid); + bidWithOptionalParams.params['bcat'] = ['IAB17-18', 'IAB7-42']; + bidWithOptionalParams.params['badv'] = ['ford.com']; + bidWithOptionalParams.params['bapp'] = ['com.foo.mygame']; + bidWithOptionalParams.params['regs'] = { + coppa: 1, + }; + + bidWithOptionalParams.userId = { + uid2: { + id: '7505e78e-4a9b-4011-8901-0e00c3f55ea9', + }, + }; + + const emptyOrtb2BidderRequest = { ortb2: {} }; + + const baseBidderRequest = { + ortb2: { + device: { + w: 1680, + h: 342, + }, + }, + }; + + const extendedBidderRequest = deepClone(baseBidderRequest); + extendedBidderRequest.ortb2['device'] = { + dnt: 0, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + lmt: 0, + ip: '192.0.0.1', + ipv6: '2001:0000:130F:0000:0000:09C0:876A:130B', + devicetype: 2, + make: 'some_producer', + model: 'some_model', + os: 'some_os', + osv: 'some_version', + js: 1, + language: 'en', + carrier: 'WiFi', + connectiontype: 0, + ifa: 'some_ifa', + geo: { + lat: 50.4, + lon: 40.2, + country: 'some_country_code', + region: 'some_region_code', + regionfips104: 'some_fips_code', + metro: 'metro_code', + city: 'city_code', + zip: 'zip_code', + type: 2, + }, + ext: { + ifatype: 'dpid', + }, + }; + extendedBidderRequest.ortb2['site'] = { + id: 'some_id', + name: 'example', + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + publisher: { + name: 'some_name', + cat: ['IAB2'], + domain: 'https://page.example.com/here.html', + }, + content: { + id: 'some_id', + episode: 22, + title: 'New episode.', + series: 'New series.', + artist: 'New artist', + genre: 'some genre', + album: 'New album', + isrc: 'AA-6Q7-20-00047', + season: 'New season', + }, + mobile: 0, + }; + extendedBidderRequest.ortb2['app'] = { + id: 'some_id', + name: 'example', + bundle: 'some_bundle', + domain: 'page.example.com', + storeurl: 'https://store.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + ver: 'some_version', + privacypolicy: 0, + paid: 0, + keywords: 'some, example, keywords', + publisher: { + name: 'some_name', + cat: ['IAB2'], + domain: 'https://page.example.com/here.html', + }, + content: { + id: 'some_id', + episode: 22, + title: 'New episode.', + series: 'New series.', + artist: 'New artist', + genre: 'some genre', + album: 'New album', + isrc: 'AA-6Q7-20-00047', + season: 'New season', + }, + }; + extendedBidderRequest.ortb2['user'] = { + yob: 1992, + gender: 'M', + }; + extendedBidderRequest.ortb2['regs'] = { + coppa: 0, + }; + + describe('isBidRequestValid', function () { + it('should return true when publisher id found', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(true); + }); + + it('should return true for video bid', () => { + expect(spec.isBidRequestValid(videoBid)).to.equal(true); + }); + + it('should return false when publisher id not found', function () { + const localBid = deepClone(bannerBid); + delete localBid.params.pubid; + delete localBid.params.floorPrice; + + expect(spec.isBidRequestValid(localBid)).to.equal(false); + }); + + it('should return false when playerSize in video not found', () => { + const localBid = deepClone(videoBid); + delete localBid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(localBid)).to.equal(false); + }); + + it('should return false when mimes in video not found', () => { + const localBid = deepClone(videoBid); + delete localBid.mediaTypes.video.mimes; + + expect(spec.isBidRequestValid(localBid)).to.equal(false); + }); + + it('should return false when protocols in video not found', () => { + const localBid = deepClone(videoBid); + delete localBid.mediaTypes.video.protocols; + + expect(spec.isBidRequestValid(localBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bannerRequest = spec.buildRequests([bannerBid], baseBidderRequest); + const bannerPayload = JSON.parse(bannerRequest[0].data); + const videoRequest = spec.buildRequests([videoBid], baseBidderRequest); + const videoPayload = JSON.parse(videoRequest[0].data); + + it('should properly map ids in request payload', function () { + expect(bannerPayload.id).to.equal(bannerBid.bidId); + expect(bannerPayload.adtagid).to.equal(bannerBid.adUnitCode); + }); + + it('should properly map banner mediaType in request payload', function () { + expect(bannerPayload.banner_properties).to.deep.equal({ + name: bannerBid.mediaTypes.banner.name, + sizes: bannerBid.mediaTypes.banner.sizes, + pos: bannerBid.mediaTypes.banner.pos, + }); + }); + + it('should properly map video mediaType in request payload', () => { + expect(videoPayload.video_properties).to.deep.equal({ + w: videoBid.mediaTypes.video.playerSize[0][0], + h: videoBid.mediaTypes.video.playerSize[0][1], + mimes: videoBid.mediaTypes.video.mimes, + minduration: videoBid.mediaTypes.video.minduration, + maxduration: videoBid.mediaTypes.video.maxduration, + protocols: videoBid.mediaTypes.video.protocols, + startdelay: videoBid.mediaTypes.video.startdelay, + placement: videoBid.mediaTypes.video.placement, + skip: videoBid.mediaTypes.video.skip, + skipafter: videoBid.mediaTypes.video.skipafter, + minbitrate: videoBid.mediaTypes.video.minbitrate, + maxbitrate: videoBid.mediaTypes.video.maxbitrate, + delivery: videoBid.mediaTypes.video.delivery, + playbackmethod: videoBid.mediaTypes.video.playbackmethod, + api: videoBid.mediaTypes.video.api, + linearity: videoBid.mediaTypes.video.linearity, + }); + }); + + it('should properly set required bidder params in request payload', function () { + expect(bannerPayload.pubid).to.equal(bannerBid.params.pubid); + expect(bannerPayload.floor_price).to.equal(bannerBid.params.floorPrice); + }); + + it('should omit optional bidder params or first-party data from bid request if they are not provided', function () { + assert.isUndefined(bannerPayload.bcat); + assert.isUndefined(bannerPayload.badv); + assert.isUndefined(bannerPayload.bapp); + assert.isUndefined(bannerPayload.user); + assert.isUndefined(bannerPayload.device); + assert.isUndefined(bannerPayload.site); + assert.isUndefined(bannerPayload.app); + assert.isUndefined(bannerPayload.user_ids); + assert.isUndefined(bannerPayload.regs); + }); + + it('should properly set optional bidder parameters', function () { + const request = spec.buildRequests( + [bidWithOptionalParams], + baseBidderRequest + ); + const payload = JSON.parse(request[0].data); + + expect(payload.bcat).to.deep.equal(['IAB17-18', 'IAB7-42']); + expect(payload.badv).to.deep.equal(['ford.com']); + expect(payload.bapp).to.deep.equal(['com.foo.mygame']); + }); + + it('should properly set optional user_ids', function () { + const request = spec.buildRequests( + [bidWithOptionalParams], + baseBidderRequest + ); + const { + user: { uid2_token }, + } = JSON.parse(request[0].data); + const expectedUID = '7505e78e-4a9b-4011-8901-0e00c3f55ea9'; + + expect(uid2_token).to.equal(expectedUID); + }); + + it('should properly set optional user_ids when no first party data is provided', function () { + const request = spec.buildRequests( + [bidWithOptionalParams], + emptyOrtb2BidderRequest + ); + const { + user: { uid2_token }, + } = JSON.parse(request[0].data); + const expectedUID = '7505e78e-4a9b-4011-8901-0e00c3f55ea9'; + + expect(uid2_token).to.equal(expectedUID); + }); + + it('should properly handle first-party data', function () { + const request = spec.buildRequests([bannerBid], extendedBidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.user).to.deep.equal(extendedBidderRequest.ortb2.user); + expect(payload.device).to.deep.equal(extendedBidderRequest.ortb2.device); + expect(payload.site).to.deep.equal(extendedBidderRequest.ortb2.site); + expect(payload.app).to.deep.equal(extendedBidderRequest.ortb2.app); + expect(payload.regs).to.deep.equal(extendedBidderRequest.ortb2.regs); + }); + + it('should not set first-party data if nothing is provided in ORTB2 param', function () { + const request = spec.buildRequests([bannerBid], emptyOrtb2BidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload).not.to.haveOwnProperty('user'); + expect(payload).not.to.haveOwnProperty('device'); + expect(payload).not.to.haveOwnProperty('site'); + expect(payload).not.to.haveOwnProperty('app'); + expect(payload).not.to.haveOwnProperty('regs'); + }); + + it('should set user id if proper cookie is present', function () { + const cookie = '157bc918-b961-4216-ac72-29fc6363edcb'; + sandbox.stub(storage, 'getCookie').returns(cookie); + + const request = spec.buildRequests([bannerBid], emptyOrtb2BidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.user.id).to.equal(cookie); + }); + + it('should not set user id if proper cookie not present', function () { + const request = spec.buildRequests([bannerBid], emptyOrtb2BidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload).not.to.haveOwnProperty('user'); + }); + }); + + describe('interpretResponse', function () { + const serverResponse = { + body: { + id: '123141241231', + bids: [ + { + cpm: 1.02, + width: 300, + height: 600, + currency: 'USD', + ad: '

Hello ad

', + creative_id: 'crid12345', + net_revenue: true, + meta: { + advertiser_domains: ['https://advertiser.com'], + }, + }, + ], + }, + }; + + const serverVideoResponse = { + body: { + id: '123141241231', + bids: [ + { + vast_url: 'https://v.a/st.xml', + cpm: 5, + width: 640, + height: 480, + currency: 'USD', + creative_id: 'crid12345', + net_revenue: true, + meta: { + advertiser_domains: ['https://advertiser.com'], + }, + }, + ], + }, + }; + + const bidRequest = { + method: 'POST', + url: 'test-url', + data: JSON.stringify({ + id: '12345', + pubid: 'somepubid', + }), + }; + + it('should properly parse response from server', function () { + const expectedResponse = { + requestId: JSON.parse(bidRequest.data).id, + cpm: serverResponse.body.bids[0].cpm, + width: serverResponse.body.bids[0].width, + height: serverResponse.body.bids[0].height, + ad: serverResponse.body.bids[0].ad, + currency: serverResponse.body.bids[0].currency, + creativeId: serverResponse.body.bids[0].creative_id, + netRevenue: serverResponse.body.bids[0].net_revenue, + meta: { + advertiserDomains: + serverResponse.body.bids[0].meta.advertiser_domains, + }, + ttl: 300, + }; + const parsedResponse = spec.interpretResponse(serverResponse, bidRequest); + + expect(parsedResponse[0]).to.deep.equal(expectedResponse); + }); + + it('should properly parse video response from server', function () { + const expectedResponse = { + requestId: JSON.parse(bidRequest.data).id, + cpm: serverVideoResponse.body.bids[0].cpm, + width: serverVideoResponse.body.bids[0].width, + height: serverVideoResponse.body.bids[0].height, + currency: serverVideoResponse.body.bids[0].currency, + creativeId: serverVideoResponse.body.bids[0].creative_id, + netRevenue: serverVideoResponse.body.bids[0].net_revenue, + mediaType: 'video', + vastUrl: serverVideoResponse.body.bids[0].vast_url, + vastXml: undefined, + meta: { + advertiserDomains: + serverVideoResponse.body.bids[0].meta.advertiser_domains, + }, + ttl: 300, + }; + const parsedResponse = spec.interpretResponse( + serverVideoResponse, + bidRequest + ); + + expect(parsedResponse[0]).to.deep.equal(expectedResponse); + }); + + it('should return empty array if no bids are returned', function () { + const emptyResponse = deepClone(serverResponse); + emptyResponse.body.bids = undefined; + + const parsedResponse = spec.interpretResponse(emptyResponse, bidRequest); + + expect(parsedResponse).to.deep.equal([]); + }); + }); + + describe('getUserSyncs', function () { + it('should return sync object with correctly injected user id', function () { + sandbox.stub(storage, 'getCookie').returns('testid'); + + const result = spec.getUserSyncs({}, {}, {}, {}); + + expect(result).to.deep.equal([ + { + type: 'image', + url: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=k1on5ig&ttd_tpi=1&ttd_puid=testid&dsp=ttd', + }, + { + type: 'image', + url: 'https://dsp.myads.telkomsel.com/api/v1/pixel?uid=testid', + }, + ]); + }); + + it('should generate user id and put the same uuid it into sync object', function () { + sandbox.stub(storage, 'getCookie').returns(undefined); + + const result = spec.getUserSyncs({}, {}, {}, {}); + const url1 = result[0].url; + const url2 = result[1].url; + + const expectedUID1 = extractValueFromURL(url1, 'ttd_puid'); + const expectedUID2 = extractValueFromURL(url2, 'uid'); + + expect(expectedUID1).to.equal(expectedUID2); + + expect(result[0]).deep.equal({ + type: 'image', + url: `https://match.adsrvr.org/track/cmf/generic?ttd_pid=k1on5ig&ttd_tpi=1&ttd_puid=${expectedUID1}&dsp=ttd`, + }); + expect(result[1]).deep.equal({ + type: 'image', + url: `https://dsp.myads.telkomsel.com/api/v1/pixel?uid=${expectedUID2}`, + }); + // Helper function to extract UUID from URL + function extractValueFromURL(url, key) { + const match = url.match(new RegExp(`[?&]${key}=([^&]*)`)); + return match ? match[1] : null; + } + }); + }); +}); diff --git a/test/spec/modules/acuityAdsBidAdapter_spec.js b/test/spec/modules/pubCircleBidAdapter_spec.js similarity index 96% rename from test/spec/modules/acuityAdsBidAdapter_spec.js rename to test/spec/modules/pubCircleBidAdapter_spec.js index 05c59036ff3..8aaa023ee1c 100644 --- a/test/spec/modules/acuityAdsBidAdapter_spec.js +++ b/test/spec/modules/pubCircleBidAdapter_spec.js @@ -1,11 +1,11 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/acuityAdsBidAdapter'; +import { spec } from '../../../modules/pubCircleBidAdapter'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'acuityads' +const bidder = 'pubcircle' -describe('AcuityAdsBidAdapter', function () { +describe('PubCircleBidAdapter', function () { const bids = [ { bidId: getUniqueIdentifierStr(), @@ -104,7 +104,7 @@ describe('AcuityAdsBidAdapter', function () { }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + expect(serverRequest.url).to.equal('https://ml.pubcircle.ai/pbjs'); }); it('Returns general data valid', function () { @@ -382,7 +382,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') }); it('Should return array of objects with proper sync config , include CCPA', function() { const syncData = spec.getUserSyncs({}, {}, {}, { @@ -393,7 +393,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&ccpa_consent=1---&coppa=0') }); }); }); diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 7d98b724bd8..5ad58ea1a37 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -72,11 +72,6 @@ describe('PublinkIdSystem', () => { expect(result.callback).to.be.a('function'); }); - it('Use local copy', () => { - const result = publinkIdSubmodule.getId({}, undefined, TEST_COOKIE_VALUE); - expect(result).to.be.undefined; - }); - describe('callout for id', () => { let callbackSpy = sinon.spy(); @@ -84,6 +79,44 @@ describe('PublinkIdSystem', () => { callbackSpy.resetHistory(); }); + it('Has cached id', () => { + const config = {storage: {type: 'cookie'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink/refresh'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + + it('Request path has priority', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + it('Fetch with consent data', () => { const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; const consentData = {gdprApplies: 1, consentString: 'myconsentstring'}; @@ -120,7 +153,7 @@ describe('PublinkIdSystem', () => { expect(parsed.search.mpn).to.equal('Prebid.js'); expect(parsed.search.mpv).to.equal('$prebid.version$'); - request.respond(204, {}, JSON.stringify(serverResponse)); + request.respond(204); expect(callbackSpy.called).to.be.false; }); diff --git a/test/spec/modules/publirBidAdapter_spec.js b/test/spec/modules/publirBidAdapter_spec.js new file mode 100644 index 00000000000..60840b82efb --- /dev/null +++ b/test/spec/modules/publirBidAdapter_spec.js @@ -0,0 +1,488 @@ +import { expect } from 'chai'; +import { spec } from 'modules/publirBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { BANNER } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT = 'https://prebid.publir.com/publirPrebidEndPoint'; +const RTB_DOMAIN_TEST = 'prebid.publir.com'; +const TTL = 360; + +describe('publirAdapter', 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('bid adapter', function () { + it('should have aliases', function () { + expect(spec.aliases).to.be.an('array').that.is.not.empty; + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'pubId': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when pubId is missing', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'pubId': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'pubId': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' + } + ]; + + const bidderRequest = { + bidderCode: 'publir', + } + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to rtbDomain ENDPOINT via POST', function () { + bidRequests[0].params.rtbDomain = RTB_DOMAIN_TEST; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send the correct bid Id', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); + config.setConfig({ + userSync: { + syncEnabled: true, + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + }); + + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); + }); + + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); + }); + + it('should check sua param in bid request', function() { + const sua = { + 'platform': { + 'brand': 'macOS', + 'version': ['12', '4', '0'] + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Not;A=Brand', + 'version': [ '99', '0', '0', '0' ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'device': { + 'sua': { + 'platform': { + 'brand': 'macOS', + 'version': [ '12', '4', '0' ] + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Not;A=Brand', + 'version': [ '99', '0', '0', '0' ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + } + } + const requestWithSua = spec.buildRequests([bid], bidderRequest); + const data = requestWithSua.data; + expect(data.bids[0].sua).to.exist; + expect(data.bids[0].sua).to.deep.equal(sua); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sua).to.not.exist; + }); + + describe('COPPA Param', function() { + it('should set coppa equal 0 in bid request if coppa is set to false', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(0); + }); + + it('should set coppa equal 1 in bid request if coppa is set to true', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'regs': { + 'coppa': true, + } + }; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(1); + }); + }); + }); + + describe('interpretResponse', function () { + const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [ + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER, + campId: '65902db45721d690ee0bc8c3' + }] + }; + + const expectedBannerResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 300, + height: 250, + ttl: TTL, + creativeId: '639153ddd0s443', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: BANNER, + meta: { + mediaType: BANNER, + ad_key: '9b5e00f2-8831-4efa-a933-c4f68710ffc0' + }, + ad: '""', + campId: '65902db45721d690ee0bc8c3', + bidder: 'publir' + }; + + it('should get correct bid response', function () { + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedBannerResponse)); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + } + }; + + const iframeSyncResponse = { + body: { + params: { + userSyncURL: 'https://iframe-sync-url.test' + } + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'pubId': 'jdye8weeyirk00000001' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) +}); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index c56ed565c43..951b5135260 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -2,10 +2,10 @@ import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalytics import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; -import { - setConfig, - addBidResponseHook, -} from 'modules/currency.js'; +import { setConfig } from 'modules/currency.js'; +import { server } from '../../mocks/xhr.js'; +import 'src/prebid.js'; +import { getGlobal } from 'src/prebidGlobal'; let events = require('src/events'); let ajax = require('src/ajax'); @@ -23,6 +23,7 @@ const { AUCTION_END, BID_REQUESTED, BID_RESPONSE, + BID_REJECTED, BIDDER_DONE, BID_WON, BID_TIMEOUT, @@ -99,7 +100,7 @@ const BID2 = Object.assign({}, BID, { adserverTargeting: { 'hb_bidder': 'pubmatic', 'hb_adid': '3bd4ebb1c900e2', - 'hb_pb': '1.500', + 'hb_pb': 1.50, 'hb_size': '728x90', 'hb_source': 'server' }, @@ -108,6 +109,9 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID2, { + rejectionReason: CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET +}) const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, @@ -237,6 +241,9 @@ const MOCK = { BID, BID2 ], + REJECTED_BID: [ + BID3 + ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' }, @@ -273,7 +280,6 @@ function getLoggerJsonFromRequest(requestBody) { describe('pubmatic analytics adapter', function () { let sandbox; - let xhr; let requests; let oldScreen; let clock; @@ -282,9 +288,7 @@ describe('pubmatic analytics adapter', function () { setUADefault(); sandbox = sinon.sandbox.create(); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); @@ -312,6 +316,208 @@ describe('pubmatic analytics adapter', function () { expect(utils.logError.called).to.equal(true); }); + describe('OW S2S', function() { + this.beforeEach(function() { + pubmaticAnalyticsAdapter.enableAnalytics({ + options: { + publisherId: 9999, + profileId: 1111, + profileVersionId: 20 + } + }); + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + }); + + this.afterEach(function() { + pubmaticAnalyticsAdapter.disableAnalytics(); + }); + + it('Pubmatic Won: No tracker fired', function() { + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(1); // only logger is fired + let request = requests[0]; + 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'); + expect(data.pdvid).to.equal('20'); + }); + + it('Non-pubmatic won: logger, tracker fired', function() { + const APPNEXUS_BID = Object.assign({}, BID, { + 'bidder': 'appnexus', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '2ecff0db240757', + 'hb_pb': 1.20, + 'hb_size': '640x480', + 'hb_source': 'server' + } + }); + + const MOCK_AUCTION_INIT_APPNEXUS = { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'appnexus', + 'params': { + 'publisherId': '1001' + } + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } + ], + 'adUnitCodes': ['/19968336/header-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'appnexus', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'appnexus', + 'params': { + 'publisherId': '1001', + 'kgpv': 'this-is-a-kgpv' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 + }; + + const MOCK_BID_REQUESTED_APPNEXUS = { + 'bidder': 'appnexus', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'appnexus', + 'adapterCode': 'appnexus', + 'bidderCode': 'appnexus', + 'params': { + 'publisherId': '1001', + 'video': { + 'minduration': 30, + 'skippable': true + } + }, + 'mediaType': 'video', + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + } + ], + 'auctionStart': 1519149536560, + 'timeout': 5000, + 'start': 1519149562216, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + }, + 'gdprConsent': { + 'consentString': 'here-goes-gdpr-consent-string', + 'gdprApplies': true + } + }; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [APPNEXUS_BID] + }); + + events.emit(AUCTION_INIT, MOCK_AUCTION_INIT_APPNEXUS); + events.emit(BID_REQUESTED, MOCK_BID_REQUESTED_APPNEXUS); + events.emit(BID_RESPONSE, APPNEXUS_BID); + events.emit(BIDDER_DONE, { + 'bidderCode': 'appnexus', + 'bids': [ + APPNEXUS_BID, + Object.assign({}, APPNEXUS_BID, { + 'serverResponseTimeMs': 42, + }) + ] + }); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, { + [APPNEXUS_BID.adUnitCode]: APPNEXUS_BID.adserverTargeting, + }); + events.emit(BID_WON, Object.assign({}, APPNEXUS_BID, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // logger as well as tracker is fired + let request = requests[1]; // 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.pubid).to.equal('9999'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + + let firstTracker = requests[0].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.pubid).to.equal('9999'); + expect(decodeURIComponent(data.purl)).to.equal('http://www.test.com/page.html'); + + expect(data.s).to.be.an('array'); + expect(data.s.length).to.equal(1); + expect(data.s[0].ps[0].pn).to.equal('appnexus'); + expect(data.s[0].ps[0].bc).to.equal('appnexus'); + }) + }); + describe('when handling events', function() { beforeEach(function () { pubmaticAnalyticsAdapter.enableAnalytics({ @@ -364,15 +570,20 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].sid).not.to.be.undefined; + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); - expect(data.s[0].ps.length).to.equal(1); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -383,9 +594,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(1); expect(data.s[0].ps[0].t).to.equal(0); @@ -393,10 +605,15 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[0].ps[0].frv).to.equal(undefined); + expect(data.s[0].ps[0].frv).to.equal(1.1); + expect(data.s[0].ps[0].pb).to.equal(1.2); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -414,7 +631,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -422,7 +640,8 @@ describe('pubmatic analytics adapter', function () { 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.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); // tracker slot1 let firstTracker = requests[0].url; @@ -452,6 +671,94 @@ describe('pubmatic analytics adapter', function () { expect(data.af).to.equal('video'); }); + it('Logger : do not log floor fields when prebids floor shows noData in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'noData'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + 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, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + 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.pubid).to.equal('9999'); + expect(data.fmv).to.equal(undefined); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(undefined); + }); + + it('Logger: log floor fields when prebids floor shows setConfig in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'setConfig'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + 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, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + 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.pubid).to.equal('9999'); + expect(data.fmv).to.equal('floorModelTest'); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + }); + it('bidCpmAdjustment: USD: Logger: best case + win tracker', function() { const bidCopy = utils.deepClone(BID); bidCopy.cpm = bidCopy.originalCpm * 2; // bidCpmAdjustment => bidCpm * 2 @@ -481,15 +788,20 @@ describe('pubmatic analytics adapter', function () { expect(data.pid).to.equal('1111'); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); expect(data.tgid).to.equal(0); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].sid).not.to.be.undefined; + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); @@ -501,7 +813,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); // tracker slot1 let firstTracker = requests[0].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -555,6 +868,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be between 0-15 else set to 0 expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -562,7 +876,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -605,6 +919,13 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be an INT between 0-15 else set to 0 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + + expect(data.s[1].sid).not.to.be.undefined; + + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); + 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); @@ -617,7 +938,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -655,7 +976,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -687,6 +1008,10 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -703,7 +1028,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(0); + expect(data.s[0].ps[0].ol1).to.equal(0); 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(1); @@ -711,7 +1037,8 @@ describe('pubmatic analytics adapter', function () { 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.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); }); it('Logger: currency conversion check', function() { @@ -748,6 +1075,7 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; 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].bc).to.equal('pubmatic'); @@ -762,7 +1090,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -794,6 +1123,10 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -810,7 +1143,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -818,7 +1152,8 @@ describe('pubmatic analytics adapter', function () { 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.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); expect(data.dvc).to.deep.equal({'plt': 2}); // respective tracker slot let firstTracker = requests[1].url; @@ -852,6 +1187,7 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; 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'); @@ -867,7 +1203,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -876,7 +1213,8 @@ describe('pubmatic analytics adapter', function () { 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}); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -905,6 +1243,10 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -921,7 +1263,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -929,7 +1272,8 @@ describe('pubmatic analytics adapter', function () { 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.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -961,6 +1305,7 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; 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'); @@ -976,7 +1321,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -992,6 +1338,66 @@ describe('pubmatic analytics adapter', function () { expect(data.kgpv).to.equal('*'); }); + it('Logger: to handle floor rejected bids', function() { + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + 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_REJECTED, MOCK.REJECTED_BID[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // 1 logger and 1 win-tracker + let request = requests[1]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + + // slot 2 + // Testing only for rejected bid as other scenarios will be covered under other TCs + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; + 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[0].ps[0].bc).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].piid).to.equal('partnerImpressionID-2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + 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(0); // Net CPM is market as 0 due to bid rejection + 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].adv).to.equal('example.com'); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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(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.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); + }); + it('Logger: best case + win tracker in case of Bidder Aliases', function() { MOCK.BID_REQUESTED['bids'][0]['bidder'] = 'pubmatic_alias'; MOCK.BID_REQUESTED['bids'][0]['bidderCode'] = 'pubmatic_alias'; @@ -1030,6 +1436,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1037,9 +1444,13 @@ describe('pubmatic analytics adapter', function () { // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic_alias'); @@ -1051,9 +1462,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1062,11 +1474,16 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); expect(data.s[0].ps[0].frv).to.equal(1.1); + expect(data.s[0].ps[0].pb).to.equal(1.2); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; 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'); @@ -1083,7 +1500,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); @@ -1091,7 +1509,8 @@ describe('pubmatic analytics adapter', function () { 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.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); + expect(data.s[1].ps[0].pb).to.equal(1.50); // tracker slot1 let firstTracker = requests[0].url; @@ -1149,6 +1568,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1156,9 +1576,13 @@ describe('pubmatic analytics adapter', function () { // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('groupm'); @@ -1170,9 +1594,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1181,10 +1606,12 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); expect(data.s[0].ps[0].frv).to.equal(1.1); + expect(data.s[0].ps[0].pb).to.equal(1.2); // slot 2 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].sid).not.to.be.undefined; 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'); @@ -1201,7 +1628,8 @@ describe('pubmatic analytics adapter', function () { 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].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).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); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 1ec9c36fac3..fda2c853e87 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -82,6 +82,7 @@ describe('PubMatic adapter', function () { ortb2Imp: { ext: { tid: '92489f71-1bf2-49a0-adf9-000cea934729', + gpid: '/1111/homepage-leftnav' } }, schain: schainConfig @@ -103,6 +104,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5890', adSlot: 'Div1@0x0', // ad_id or tagid + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -153,6 +155,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5890', adSlot: 'Div1@640x480', // ad_id or tagid + wiid: '1234567890', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -212,6 +215,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' }, bidId: '2a5571261281d4', requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', @@ -277,6 +281,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' }, bidId: '2a5571261281d4', requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', @@ -303,6 +308,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' } }]; @@ -343,6 +349,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' } }]; @@ -501,6 +508,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '301', adSlot: '/15671365/DMDemo@300x250:0', + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -571,6 +579,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '301', adSlot: '/15671365/DMDemo@300x250:0', + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -1133,7 +1142,19 @@ describe('PubMatic adapter', function () { ortb2: { source: { tid: 'source-tid' - } + }, + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, } }); let data = JSON.parse(request.data); @@ -1144,10 +1165,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.source.tid).to.equal('source-tid'); // Prebid TransactionId @@ -1160,6 +1181,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1387,7 +1409,21 @@ describe('PubMatic adapter', function () { it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }); let data = JSON.parse(request.data); expect(data.at).to.equal(1); // auction type @@ -1397,10 +1433,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1413,6 +1449,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].banner.w).to.equal(728); // width expect(data.imp[0].banner.h).to.equal(90); // height expect(data.imp[0].banner.format).to.deep.equal([{w: 160, h: 600}]); + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1596,7 +1633,21 @@ describe('PubMatic adapter', function () { it('Pass auctiondId as wiid if wiid is not passed in params', function () { let bidRequest = { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }; delete bidRequests[0].params.wiid; let request = spec.buildRequests(bidRequests, bidRequest); @@ -1608,10 +1659,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Longitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Longitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal('new-auction-id'); // OpenWrap: Wrapper Impression ID @@ -1623,6 +1674,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid }); @@ -1631,6 +1683,20 @@ describe('PubMatic adapter', function () { gdprConsent: { consentString: 'kjfdniwjnifwenrif3', gdprApplies: true + }, + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, } }; let request = spec.buildRequests(bidRequests, bidRequest); @@ -1644,10 +1710,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1657,6 +1723,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid @@ -1664,7 +1731,21 @@ describe('PubMatic adapter', function () { it('Request params check with USP/CCPA Consent', function () { let bidRequest = { - uspConsent: '1NYN' + uspConsent: '1NYN', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }; let request = spec.buildRequests(bidRequests, bidRequest); let data = JSON.parse(request.data); @@ -1676,10 +1757,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1691,6 +1772,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid // second request without USP/CCPA @@ -1699,6 +1781,37 @@ describe('PubMatic adapter', function () { expect(data2.regs).to.equal(undefined);// USP/CCPAs }); + it('Request params should include DSA signals if present', function () { + const dsa = { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [ + { + domain: 'platform1domain.com', + dsaparams: [1] + }, + { + domain: 'SSP2domain.com', + dsaparams: [1, 2] + } + ] + }; + + let bidRequest = { + ortb2: { + regs: { + ext: { + dsa + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + assert.deepEqual(data.regs.ext.dsa, dsa); + }); + it('Request params check with JW player params', function() { let bidRequests = [ { @@ -1840,7 +1953,43 @@ describe('PubMatic adapter', function () { expect(data.user.yob).to.equal(1985); }); + it('ortb2.badv should be merged in the request', function() { + const ortb2 = { + badv: ['example.com'] + }; + const request = spec.buildRequests(bidRequests, {ortb2}); + let data = JSON.parse(request.data); + expect(data.badv).to.deep.equal(['example.com']); + }); + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.gpid', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should send gpid if imp[].ext.gpid is specified', function() { + bidRequests[0].ortb2Imp = { + ext: { + gpid: 'ortb2Imp.ext.gpid' + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.have.property('gpid'); + expect(data.imp[0].ext.gpid).to.equal('ortb2Imp.ext.gpid'); + }); + + it('should not send if imp[].ext.gpid is not specified', function() { + bidRequests[0].ortb2Imp = { ext: { } }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('gpid'); + }); + }); + describe('ortb2Imp.ext.data.pbadslot', function() { beforeEach(function () { if (bidRequests[0].hasOwnProperty('ortb2Imp')) { @@ -2210,6 +2359,23 @@ describe('PubMatic adapter', function () { expect(data.device.sua).to.deep.equal(suaObject); }); + it('should pass device.ext.cdep if present in bidderRequest fpd ortb2 object', function () { + const cdepObj = { + cdep: 'example_label_1' + }; + let request = spec.buildRequests(multipleMediaRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: cdepObj + } + } + }); + let data = JSON.parse(request.data); + expect(data.device.ext.cdep).to.exist.and.to.be.an('string'); + expect(data.device.ext).to.deep.equal(cdepObj); + }); + it('Request params should have valid native bid request for all valid params', function () { let request = spec.buildRequests(nativeBidRequests, { auctionId: 'new-auction-id' @@ -2325,7 +2491,21 @@ describe('PubMatic adapter', function () { it('Request params check for 1 banner and 1 video ad', function () { let request = spec.buildRequests(multipleMediaRequests, { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }); let data = JSON.parse(request.data); @@ -2339,10 +2519,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(multipleMediaRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(multipleMediaRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(multipleMediaRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(multipleMediaRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(multipleMediaRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(multipleMediaRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(multipleMediaRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(multipleMediaRequests[0].transactionId); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(multipleMediaRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -3519,8 +3699,101 @@ describe('PubMatic adapter', function () { } }); + describe('Fledge Auction config Response', function () { + let response; + let bidRequestConfigs = [ + { + bidder: 'pubmatic', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + params: { + publisherId: '5670', + adSlot: '/15671365/DMDemo@300x250:0', + kadfloor: '1.2', + pmzoneid: 'aabc, ddef', + kadpageurl: 'www.publisher.com', + yob: '1986', + gender: 'M', + lat: '12.3', + lon: '23.7', + wiid: '1234567890', + profId: '100', + verId: '200', + currency: 'AUD', + dctr: 'key1:val1,val2|key2:val1' + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: 'test_bid_id', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + ortb2Imp: { + ext: { + tid: '92489f71-1bf2-49a0-adf9-000cea934729', + ae: 1 + } + }, + } + ]; + + let bidRequest = spec.buildRequests(bidRequestConfigs, {}); + let bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test_bid_id', + price: 2, + w: 728, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS', + ext: { + fledge_auction_configs: { + 'test_bid_id': { + seller: 'ads.pubmatic.com', + interestGroupBuyers: ['dsp1.com'], + sellerTimeout: 0, + perBuyerSignals: { + 'dsp1.com': { + bid_macros: 0.1, + disallowed_adv_ids: [ + '5678', + '5890' + ], + } + } + } + } + } + }; + + response = spec.interpretResponse({ body: bidResponse }, bidRequest); + it('should return FLEDGE auction_configs alongside bids', function () { + expect(response).to.have.property('bids'); + expect(response).to.have.property('fledgeAuctionConfigs'); + expect(response.fledgeAuctionConfigs.length).to.equal(1); + expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test_bid_id'); + }); + }); + describe('Preapare metadata', function () { it('Should copy all fields from ext to meta', function () { + const dsa = { + behalf: 'Advertiser', + paid: 'Advertiser', + transparency: [{ + domain: 'dsp1domain.com', + dsaparams: [1, 2] + }], + adrender: 1 + }; + const bid = { 'adomain': [ 'mystartab.com' @@ -3532,6 +3805,7 @@ describe('PubMatic adapter', function () { 'deal_channel': 1, 'bidtype': 0, advertiserId: 'adid', + dsa, // networkName: 'nwnm', // primaryCatId: 'pcid', // advertiserName: 'adnm', @@ -3563,6 +3837,7 @@ describe('PubMatic adapter', function () { expect(br.meta.secondaryCatIds[0]).to.equal('IAB_CATEGORY'); expect(br.meta.advertiserDomains).to.be.an('array').with.length.above(0); // adomain expect(br.meta.clickUrl).to.equal('mystartab.com'); // adomain + expect(br.meta.dsa).to.equal(dsa); // dsa }); it('Should be empty, when ext and adomain is absent in bid object', function () { diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index e14582edc39..92d5972cc13 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import pubwiseAnalytics from 'modules/pubwiseAnalyticsAdapter.js'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let adapterManager = require('src/adapterManager').default; @@ -9,7 +10,6 @@ let constants = require('src/constants.json'); describe('PubWise Prebid Analytics', function () { let requests; let sandbox; - let xhr; let clock; let mock = {}; @@ -38,9 +38,7 @@ describe('PubWise Prebid Analytics', function () { clock = sandbox.useFakeTimers(); sandbox.stub(events, 'getEvents').returns([]); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; }); afterEach(function () { @@ -50,10 +48,6 @@ describe('PubWise Prebid Analytics', function () { }); describe('enableAnalytics', function () { - beforeEach(function () { - requests = []; - }); - it('should catch all events', function () { pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 1dce87e4b8e..e0f4497a8c8 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -1,13 +1,9 @@ -import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; -import { getDeviceType, getBrowser, getOS } from 'modules/pubxaiAnalyticsAdapter.js'; -import { - expect -} from 'chai'; +import pubxaiAnalyticsAdapter, {getBrowser, getDeviceType, getOS} from 'modules/pubxaiAnalyticsAdapter.js'; +import {expect} from 'chai'; import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { - server -} from 'test/mocks/xhr.js'; +import {server} from 'test/mocks/xhr.js'; +import {getGptSlotInfoForAdUnitCode} from '../../../libraries/gptUtils/gptUtils.js'; let events = require('src/events'); let constants = require('src/constants.json'); @@ -527,7 +523,7 @@ describe('pubxai analytics adapter', function() { 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'sizes': '300x250', 'renderStatus': 2, @@ -596,7 +592,7 @@ describe('pubxai analytics adapter', function() { let expectedAfterBidWon = { 'winningBid': { 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', diff --git a/test/spec/modules/pxyzBidAdapter_spec.js b/test/spec/modules/pxyzBidAdapter_spec.js index 3a336c86e46..36e7a1e9ad6 100644 --- a/test/spec/modules/pxyzBidAdapter_spec.js +++ b/test/spec/modules/pxyzBidAdapter_spec.js @@ -210,30 +210,15 @@ describe('pxyzBidAdapter', function () { }); describe('getUserSyncs', function () { - const syncUrl = '//ib.adnxs.com/getuidnb?https://ads.playground.xyz/usersync?partner=appnexus&uid=$UID'; - - describe('when iframeEnabled is true', function () { - const syncOptions = { - 'iframeEnabled': true - } - it('should return one image type user sync pixel', function () { - let result = spec.getUserSyncs(syncOptions); - expect(result.length).to.equal(1); - expect(result[0].type).to.equal('image') - expect(result[0].url).to.equal(syncUrl); - }); - }); - - describe('when iframeEnabled is false', function () { - const syncOptions = { - 'iframeEnabled': false - } - it('should return one image type user sync pixel', function () { - let result = spec.getUserSyncs(syncOptions); - expect(result.length).to.equal(1); - expect(result[0].type).to.equal('image') - expect(result[0].url).to.equal(syncUrl); - }); + const syncImageUrl = '//ib.adnxs.com/getuidnb?https://ads.playground.xyz/usersync?partner=appnexus&uid=$UID'; + const syncIframeUrl = '//rtb.gumgum.com/getuid/15801?r=https%3A%2F%2Fads.playground.xyz%2Fusersync%3Fpartner%3Dgumgum%26uid%3D'; + it('should return one image type user sync pixel', function () { + let result = spec.getUserSyncs(); + expect(result.length).to.equal(2); + expect(result[0].type).to.equal('image') + expect(result[0].url).to.equal(syncImageUrl); + expect(result[1].type).to.equal('iframe') + expect(result[1].url).to.equal(syncIframeUrl); }); }) }); diff --git a/test/spec/modules/qortexRtdProvider_spec.js b/test/spec/modules/qortexRtdProvider_spec.js new file mode 100644 index 00000000000..9baa526e4cc --- /dev/null +++ b/test/spec/modules/qortexRtdProvider_spec.js @@ -0,0 +1,333 @@ +import * as utils from 'src/utils'; +import * as ajax from 'src/ajax.js'; +import * as events from 'src/events.js'; +import CONSTANTS from '../../../src/constants.json'; +import {loadExternalScript} from 'src/adloader.js'; +import { + qortexSubmodule as module, + getContext, + addContextToRequests, + setContextData, + initializeModuleData, + loadScriptTag +} from '../../../modules/qortexRtdProvider'; +import {server} from '../../mocks/xhr.js'; +import { cloneDeep } from 'lodash'; + +describe('qortexRtdProvider', () => { + let logWarnSpy; + let ortb2Stub; + + const defaultApiHost = 'https://demand.qortex.ai'; + const defaultGroupId = 'test'; + + const validBidderArray = ['qortex', 'test']; + const validTagConfig = { + videoContainer: 'my-video-container' + } + + const validModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray + } + }, + emptyModuleConfig = { + params: {} + } + + const validImpressionEvent = { + detail: { + uid: 'uid123', + type: 'qx-impression' + } + }, + validImpressionEvent2 = { + detail: { + uid: 'uid1234', + type: 'qx-impression' + } + }, + missingIdImpressionEvent = { + detail: { + type: 'qx-impression' + } + }, + invalidTypeQortexEvent = { + detail: { + type: 'invalid-type' + } + } + + const responseHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*' + }; + + const responseObj = { + content: { + id: '123456', + episode: 15, + title: 'test episode', + series: 'test show', + season: '1', + url: 'https://example.com/file.mp4' + } + }; + + const apiResponse = JSON.stringify(responseObj); + + const reqBidsConfig = { + adUnits: [{ + bids: [ + { bidder: 'qortex' } + ] + }], + ortb2Fragments: { + bidder: {}, + global: {} + } + } + + beforeEach(() => { + ortb2Stub = sinon.stub(reqBidsConfig, 'ortb2Fragments').value({bidder: {}, global: {}}) + logWarnSpy = sinon.spy(utils, 'logWarn'); + }) + + afterEach(() => { + logWarnSpy.restore(); + ortb2Stub.restore(); + setContextData(null); + }) + + describe('init', () => { + it('returns true for valid config object', () => { + expect(module.init(validModuleConfig)).to.be.true; + }) + + it('returns false and logs error for missing groupId', () => { + expect(module.init(emptyModuleConfig)).to.be.false; + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('Qortex RTD module config does not contain valid groupId parameter. Config params: {}')).to.be.ok; + }) + + it('loads Qortex script if tagConfig is present in module config params', () => { + const config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + expect(module.init(config)).to.be.true; + expect(loadExternalScript.calledOnce).to.be.true; + }) + }) + + describe('loadScriptTag', () => { + let addEventListenerSpy; + let billableEvents = []; + + let config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, (e) => { + billableEvents.push(e); + }) + + beforeEach(() => { + initializeModuleData(config); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }) + + afterEach(() => { + addEventListenerSpy.restore(); + billableEvents = []; + }) + + it('adds event listener', () => { + loadScriptTag(config); + expect(addEventListenerSpy.calledOnce).to.be.true; + }) + + it('parses incoming qortex-impression events', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(billableEvents[0].type).to.be.equal(validImpressionEvent.detail.type); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + }) + + it('will emit two events for impressions with two different ids', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent2)); + expect(billableEvents.length).to.be.equal(2); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + expect(billableEvents[1].transactionId).to.be.equal(validImpressionEvent2.detail.uid); + }) + + it('will not allow multiple events with the same id', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(logWarnSpy.calledWith('received invalid billable event due to duplicate uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with missing uid', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', missingIdImpressionEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event due to missing uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with unavailable type', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', invalidTypeQortexEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event: invalid-type')).to.be.ok; + }) + }) + + describe('getBidRequestData', () => { + let callbackSpy; + + beforeEach(() => { + initializeModuleData(validModuleConfig); + callbackSpy = sinon.spy(); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + callbackSpy.resetHistory(); + }) + + it('will call callback immediately if no adunits', () => { + const reqBidsConfigNoBids = { adUnits: [] }; + module.getBidRequestData(reqBidsConfigNoBids, callbackSpy); + expect(callbackSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfigNoBids))).to.be.ok; + }) + + it('will call callback if getContext does not throw', () => { + const cb = function () { + expect(logWarnSpy.calledOnce).to.be.false; + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + server.requests[0].respond(200, responseHeaders, apiResponse); + }) + + it('will catch and log error and fire callback', (done) => { + const a = sinon.stub(ajax, 'ajax').throws(new Error('test')); + const cb = function () { + expect(logWarnSpy.calledWith('test')).to.be.eql(true); + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + a.restore(); + }) + }) + + describe('getContext', () => { + beforeEach(() => { + initializeModuleData(validModuleConfig); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + }) + + it('returns a promise', (done) => { + const result = getContext(); + expect(result).to.be.a('promise'); + done(); + }) + + it('uses request url generated from initialize function in config and resolves to content object data', (done) => { + let requestUrl = `${validModuleConfig.params.apiUrl}/api/v1/analyze/${validModuleConfig.params.groupId}/prebid`; + const ctx = getContext() + expect(server.requests.length).to.be.eql(1); + expect(server.requests[0].url).to.be.eql(requestUrl); + server.requests[0].respond(200, responseHeaders, apiResponse); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('will return existing context data instead of ajax call if the source was not updated', (done) => { + setContextData(responseObj.content); + const ctx = getContext(); + expect(server.requests.length).to.be.eql(0); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('returns null for non erroring api responses other than 200', (done) => { + const nullContentResponse = { content: null } + const ctx = getContext() + server.requests[0].respond(200, responseHeaders, JSON.stringify(nullContentResponse)) + ctx.then(response => { + expect(response).to.be.null; + expect(server.requests.length).to.be.eql(1); + expect(logWarnSpy.called).to.be.false; + done(); + }); + }) + }) + + describe(' addContextToRequests', () => { + it('logs error if no data was retrieved from get context call', () => { + initializeModuleData(validModuleConfig); + addContextToRequests(reqBidsConfig); + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No context data received at this time')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to global ortb2 when bidders array is omitted', () => { + const omittedBidderArrayConfig = cloneDeep(validModuleConfig); + delete omittedBidderArrayConfig.params.bidders; + initializeModuleData(omittedBidderArrayConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + expect(reqBidsConfig.ortb2Fragments.global).to.have.property('site'); + expect(reqBidsConfig.ortb2Fragments.global.site).to.have.property('content'); + expect(reqBidsConfig.ortb2Fragments.global.site.content).to.be.eql(responseObj.content); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to bidder ortb2 when bidders array is included', () => { + initializeModuleData(validModuleConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + + const qortexOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['qortex'] + expect(qortexOrtb2Fragment).to.not.be.null; + expect(qortexOrtb2Fragment).to.have.property('site'); + expect(qortexOrtb2Fragment.site).to.have.property('content'); + expect(qortexOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + const testOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['test'] + expect(testOrtb2Fragment).to.not.be.null; + expect(testOrtb2Fragment).to.have.property('site'); + expect(testOrtb2Fragment.site).to.have.property('content'); + expect(testOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + }) + + it('logs error if there is an empty bidder array', () => { + const invalidBidderArrayConfig = cloneDeep(validModuleConfig); + invalidBidderArrayConfig.params.bidders = []; + initializeModuleData(invalidBidderArrayConfig); + setContextData(responseObj.content) + addContextToRequests(reqBidsConfig); + + expect(logWarnSpy.calledWith('Config contains an empty bidders array, unable to determine which bids to enrich')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + }) +}) diff --git a/test/spec/modules/r2b2BidAdapter_spec.js b/test/spec/modules/r2b2BidAdapter_spec.js new file mode 100644 index 00000000000..b94b400a71d --- /dev/null +++ b/test/spec/modules/r2b2BidAdapter_spec.js @@ -0,0 +1,689 @@ +import {expect} from 'chai'; +import {spec, internal as r2b2, internal} from 'modules/r2b2BidAdapter.js'; +import * as utils from '../../../src/utils'; +import 'modules/schain.js'; +import 'modules/userId/index.js'; + +function encodePlacementIds (ids) { + return btoa(JSON.stringify(ids)); +} + +describe('R2B2 adapter', function () { + let serverResponse, requestForInterpretResponse; + let bidderRequest; + let bids = []; + let gdprConsent = { + gdprApplies: true, + consentString: 'consent-string', + }; + let schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }; + const usPrivacyString = '1YNN'; + const impId = 'impID'; + const price = 10.6; + const ad = 'adm'; + const creativeId = 'creativeID'; + const cid = 41849; + const cdid = 595121; + const unitCode = 'unitCode'; + const bidId1 = '1'; + const bidId2 = '2'; + const bidId3 = '3'; + const bidId4 = '4'; + const bidId5 = '5'; + const bidWonUrl = 'url1'; + const setTargetingUrl = 'url2'; + const bidder = 'r2b2'; + const foreignBidder = 'differentBidder'; + const id1 = { pid: 'd/g/p' }; + const id1Object = { d: 'd', g: 'g', p: 'p', m: 0 }; + const id2 = { pid: 'd/g/p/1' }; + const id2Object = { d: 'd', g: 'g', p: 'p', m: 1 }; + const badId = { pid: 'd/g/' }; + const bid1 = { bidId: bidId1, bidder, params: [ id1 ] }; + const bid2 = { bidId: bidId2, bidder, params: [ id2 ] }; + const bidWithBadSetup = { bidId: bidId3, bidder, params: [ badId ] }; + const bidForeign1 = { bidId: bidId4, bidder: foreignBidder, params: [ { id: 'abc' } ] }; + const bidForeign2 = { bidId: bidId5, bidder: foreignBidder, params: [ { id: 'xyz' } ] }; + const fakeTime = 1234567890; + const cacheBusterRegex = /[\?&]cb=([^&]+)/; + let bidStub, time; + + beforeEach(function () { + bids = [{ + bidder: 'r2b2', + params: { + pid: 'example.com/generic/300x250/1' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + adUnitCode: unitCode, + transactionId: '29c408b9-65ce-48b1-9167-18a57791f908', + sizes: [ + [300, 250] + ], + bidId: '20917a54ee9858', + bidderRequestId: '15270d403778d', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + src: 'client', + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + schain + }, { + bidder: 'r2b2', + params: { + pid: 'example.com/generic/300x600/0' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + adUnitCode: unitCode, + transactionId: '29c408b9-65ce-48b1-9167-18a57791f908', + sizes: [ + [300, 600] + ], + bidId: '3dd53d30c691fe', + bidderRequestId: '15270d403778d', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + src: 'client', + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + schain + }]; + bidderRequest = { + bidderCode: 'r2b2', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + bidderRequestId: '15270d403778d', + bids: bids, + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + gdprConsent: { + consentString: 'consent-string', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }, + uspConsent: '1YYY', + }; + serverResponse = { + id: 'a66a6e32-2a7d-4ed3-bb13-6f3c9bdcf6a1', + seatbid: [{ + bid: [{ + id: '4756cc9e9b504fd0bd39fdd594506545', + impid: impId, + price: price, + adm: ad, + crid: creativeId, + w: 300, + h: 250, + ext: { + prebid: { + meta: { + adaptercode: 'r2b2' + }, + type: 'banner' + }, + r2b2: { + cdid: cdid, + cid: cid, + useRenderer: true + } + } + }], + seat: 'seat' + }] + }; + requestForInterpretResponse = { + data: { + imp: [ + {id: impId} + ] + }, + bids + }; + }); + + describe('isBidRequestValid', function () { + let bid = {}; + + it('should return false when missing required "pid" param', function () { + bid.params = {random: 'param'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {d: 'd', g: 'g', p: 'p', m: 1}; + expect(spec.isBidRequestValid(bid)).to.equal(false) + }); + + it('should return false when "pid" is malformed', function () { + bid.params = {pid: 'pid'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: '///'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: '/g/p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd//p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/g//m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/p/'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/g/p/m/t'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when "pid" is a correct dgpm', function () { + bid.params = {pid: 'd/g/p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when type is blank', function () { + bid.params = {pid: 'd/g/p/'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when type is missing', function () { + bid.params = {pid: 'd/g/p'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when "pid" is a number', function () { + bid.params = {pid: 12356}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when "pid" is a numeric string', function () { + bid.params = {pid: '12356'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true for selfpromo unit', function () { + bid.params = {pid: 'selfpromo'}; + expect(spec.isBidRequestValid(bid)).to.equal(true) + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + r2b2.placementsToSync = []; + r2b2.mappedParams = {}; + }); + + it('should set correct request method and url and pass bids', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let request = requests[0] + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://hb.r2b2.cz/openrtb2/bid'); + expect(request.data).to.be.an('object'); + expect(request.bids).to.deep.equal(bids); + }); + + it('should pass correct parameters', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp, device, site, source, ext, cur, test} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(device).to.be.an('object'); + expect(site).to.be.an('object'); + expect(source).to.be.an('object'); + expect(cur).to.deep.equal(['USD']); + expect(ext.version).to.equal('1.0.0'); + expect(test).to.equal(0); + }); + + it('should pass correct imp', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.id).to.equal('20917a54ee9858'); + expect(bid.banner).to.deep.equal({topframe: 0, format: [{w: 300, h: 250}]}); + expect(bid.ext).to.be.an('object'); + expect(bid.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1}); + }); + + it('should map type correctly', function () { + let result, bid; + let requestWithId = function(id) { + let b = bids[0]; + b.params.pid = id; + let passedBids = [b]; + bidderRequest.bids = passedBids; + return spec.buildRequests(passedBids, bidderRequest); + }; + + result = requestWithId('example.com/generic/300x250/mobile'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250/desktop'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + + result = requestWithId('example.com/generic/300x250/1'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250/0'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + + result = requestWithId('example.com/generic/300x250/m'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + }); + + it('should pass correct parameters for test ad', function () { + let testAdBid = bids[0]; + testAdBid.params = {pid: 'selfpromo'}; + let requests = spec.buildRequests([testAdBid], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.ext).to.be.an('object'); + expect(bid.ext.r2b2).to.deep.equal({d: 'test', g: 'test', p: 'selfpromo', m: 0, 'selfpromo': 1}); + }); + + it('should pass multiple bids', function () { + let requests = spec.buildRequests(bids, bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(bids.length); + let bid1 = imp[0]; + expect(bid1.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1}); + let bid2 = imp[1]; + expect(bid2.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x600', m: 0}); + }); + + it('should set up internal variables', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let bid1Id = bids[0].bidId; + let bid2Id = bids[1].bidId; + expect(r2b2.placementsToSync).to.be.an('array').that.has.lengthOf(2); + expect(r2b2.mappedParams).to.have.property(bid1Id); + expect(r2b2.mappedParams[bid1Id]).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1, pid: 'example.com/generic/300x250/1'}); + expect(r2b2.mappedParams).to.have.property(bid2Id); + expect(r2b2.mappedParams[bid2Id]).to.deep.equal({d: 'example.com', g: 'generic', p: '300x600', m: 0, pid: 'example.com/generic/300x600/0'}); + }); + + it('should pass gdpr properties', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {user, regs} = data; + expect(user).to.be.an('object').that.has.property('ext'); + expect(regs).to.be.an('object').that.has.property('ext'); + expect(user.ext.consent).to.equal('consent-string'); + expect(regs.ext.gdpr).to.equal(1); + }); + + it('should pass us privacy properties', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {regs} = data; + expect(regs).to.be.an('object').that.has.property('ext'); + expect(regs.ext.us_privacy).to.equal('1YYY'); + }); + + it('should pass supply chain', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {source} = data; + expect(source).to.be.an('object').that.has.property('ext'); + expect(source.ext.schain).to.deep.equal({ + complete: 1, + nodes: [ + {asi: 'example.com', hp: 1, sid: '00001'} + ], + ver: '1.0' + }) + }); + + it('should pass extended ids', function () { + let eidsArray = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID', + }, + id: 'TTD_ID_FROM_USER_ID_MODULE', + }, + ], + }, + { + source: 'pubcid.org', + uids: [ + { + atype: 1, + id: 'pubCommonId_FROM_USER_ID_MODULE', + }, + ], + }, + ]; + bids[0].userIdAsEids = eidsArray; + let requests = spec.buildRequests(bids, bidderRequest); + let request = requests[0]; + let eids = request.data.user.ext.eids; + + expect(eids).to.deep.equal(eidsArray); + }); + }); + + describe('interpretResponse', function () { + it('should respond with empty response when there are no bids', function () { + let result = spec.interpretResponse({ body: {} }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [ {} ] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [ { bids: [] } ] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + }); + + it('should map params correctly', function () { + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.requestId).to.equal(impId); + expect(bid.cpm).to.equal(price); + expect(bid.ad).to.equal(ad); + expect(bid.currency).to.equal('USD'); + expect(bid.mediaType).to.equal('banner'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.creativeId).to.equal(creativeId); + }); + + it('should set up renderer on bid', function () { + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.renderer).to.be.an('object'); + expect(bid.renderer).to.have.property('render').that.is.a('function'); + expect(bid.renderer).to.have.property('url').that.is.a('string'); + }); + + it('should map ext params correctly', function() { + let dgpm = {something: 'something'}; + r2b2.mappedParams = {}; + r2b2.mappedParams[impId] = dgpm; + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.ext).to.be.an('object'); + let { ext } = bid; + expect(ext.dgpm).to.deep.equal(dgpm); + expect(ext.cid).to.equal(cid); + expect(ext.cdid).to.equal(cdid); + expect(ext.adUnit).to.equal(unitCode); + expect(ext.mediaType).to.deep.equal({ + type: 'banner', + settings: { + chd: null, + width: 300, + height: 250, + ad: { + type: 'content', + data: ad + } + } + }); + }); + + it('should handle multiple bids', function() { + const impId2 = '123456'; + const price2 = 12; + const ad2 = 'gaeouho'; + const w2 = 300; + const h2 = 600; + let b = serverResponse.seatbid[0].bid[0]; + let b2 = Object.assign({}, b); + b2.impid = impId2; + b2.price = price2; + b2.adm = ad2; + b2.w = w2; + b2.h = h2; + serverResponse.seatbid[0].bid.push(b2); + requestForInterpretResponse.data.imp.push({id: impId2}); + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(2); + let firstBid = result[0]; + let secondBid = result[1]; + expect(firstBid.requestId).to.equal(impId); + expect(firstBid.ad).to.equal(ad); + expect(firstBid.cpm).to.equal(price); + expect(firstBid.width).to.equal(300); + expect(firstBid.height).to.equal(250); + expect(secondBid.requestId).to.equal(impId2); + expect(secondBid.ad).to.equal(ad2); + expect(secondBid.cpm).to.equal(price2); + expect(secondBid.width).to.equal(w2); + expect(secondBid.height).to.equal(h2); + }); + }); + + describe('getUserSyncs', function() { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + + it('should return an array with a sync for all bids', function() { + r2b2.placementsToSync = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(r2b2.placementsToSync); + const syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.be.an('array').that.has.lengthOf(1); + const sync = syncs[0]; + expect(sync).to.be.an('object'); + expect(sync.type).to.equal('iframe'); + expect(sync.url).to.include(`?p=${expectedEncodedIds}`); + }); + + it('should return the sync and include gdpr and usp parameters in the url', function() { + r2b2.placementsToSync = [id1Object, id2Object]; + const syncs = spec.getUserSyncs(syncOptions, {}, gdprConsent, usPrivacyString); + const sync = syncs[0]; + expect(sync).to.be.an('object'); + expect(sync.url).to.include(`&gdpr=1`); + expect(sync.url).to.include(`&gdpr_consent=${gdprConsent.consentString}`); + expect(sync.url).to.include(`&us_privacy=${usPrivacyString}`); + }); + }); + + describe('events', function() { + beforeEach(function() { + time = sinon.useFakeTimers(fakeTime); + sinon.stub(utils, 'triggerPixel'); + r2b2.mappedParams = {}; + r2b2.mappedParams[bidId1] = id1Object; + r2b2.mappedParams[bidId2] = id2Object; + bidStub = { + adserverTargeting: { hb_bidder: bidder, hb_pb: '10.00', hb_size: '300x300' }, + cpm: 10, + currency: 'USD', + ext: { + dgpm: { d: 'r2b2.cz', g: 'generic', m: 1, p: '300x300', pid: 'r2b2.cz/generic/300x300/1' } + }, + params: [ { pid: 'r2b2.cz/generic/300x300/1' } ], + }; + }); + afterEach(function() { + utils.triggerPixel.restore(); + time.restore(); + }); + + describe('onBidWon', function () { + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel with passed url', function () { + bidStub.ext.events = { + onBidWon: bidWonUrl, + onSetTargeting: setTargetingUrl + }; + const response = spec.onBidWon(bidStub); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.calledWithMatch(bidWonUrl)).to.equal(true); + }); + it('should not trigger a pixel if url is not available', function () { + bidStub.ext.events = null; + spec.onBidWon(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + bidStub.ext.events = { + onBidWon: '', + onSetTargeting: '', + }; + spec.onBidWon(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); + + describe('onSetTargeting', function () { + it('exists and is a function', () => { + expect(spec.onSetTargeting).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel with passed url', function () { + bidStub.ext.events = { + onBidWon: bidWonUrl, + onSetTargeting: setTargetingUrl + }; + const response = spec.onSetTargeting(bidStub); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.calledWithMatch(setTargetingUrl)).to.equal(true); + }); + it('should not trigger a pixel if url is not available', function () { + bidStub.ext.events = null; + spec.onSetTargeting(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + bidStub.ext.events = { + onBidWon: '', + onSetTargeting: '', + }; + spec.onSetTargeting(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); + + describe('onTimeout', function () { + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel', function () { + const bids = [bid1, bid2]; + const response = spec.onTimeout(bids); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.callCount).to.equal(1); + }); + it('should not trigger a pixel if no bids available', function () { + const bids = []; + spec.onTimeout(bids); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + it('should trigger a pixel with correct ids and a cache buster', function () { + const bids = [bid1, bidForeign1, bidForeign2, bid2, bidWithBadSetup]; + const expectedIds = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(expectedIds); + spec.onTimeout(bids); + expect(utils.triggerPixel.callCount).to.equal(1); + const triggeredUrl = utils.triggerPixel.args[0][0]; + expect(triggeredUrl).to.include(`p=${expectedEncodedIds}`); + expect(triggeredUrl.match(cacheBusterRegex)).to.exist; + }); + }); + + describe('onBidderError', function () { + it('exists and is a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel', function () { + const bidderRequest = { bids: [bid1, bid2] }; + const response = spec.onBidderError({ bidderRequest }); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.callCount).to.equal(1); + }); + it('should not trigger a pixel if no bids available', function () { + const bidderRequest = { bids: [] }; + spec.onBidderError({ bidderRequest }); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + it('should call triggerEvent with correct ids and a cache buster', function () { + const bids = [bid1, bid2, bidWithBadSetup] + const bidderRequest = { bids }; + const expectedIds = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(expectedIds); + spec.onBidderError({ bidderRequest }); + expect(utils.triggerPixel.callCount).to.equal(1); + const triggeredUrl = utils.triggerPixel.args[0][0]; + expect(triggeredUrl).to.include(`p=${expectedEncodedIds}`); + expect(triggeredUrl.match(cacheBusterRegex)).to.exist; + }); + }); + }); +}); diff --git a/test/spec/modules/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index bfa72a2510e..f172d192221 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -63,6 +63,20 @@ describe('rasBidAdapter', function () { customParams: { test: 'name=value' } + }, + mediaTypes: { + banner: { + sizes: [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } } }; const bid2 = { @@ -74,6 +88,16 @@ describe('rasBidAdapter', function () { area: 'areatest', site: 'test', network: '4178463' + }, + mediaTypes: { + banner: { + sizes: [ + [ + 750, + 300 + ] + ] + } } }; @@ -127,6 +151,7 @@ describe('rasBidAdapter', function () { } }; const requests = spec.buildRequests([bidCopy, bid2]); + expect(requests[0].url).to.have.string(CSR_ENDPOINT); expect(requests[0].url).to.have.string('slot0=test'); expect(requests[0].url).to.have.string('id0=1'); @@ -147,6 +172,39 @@ describe('rasBidAdapter', function () { expect(requests[0].url).to.have.string('kvadunit=test%2Fareatest'); expect(requests[0].url).to.have.string('pos0=0'); }); + + it('should parse dsainfo when available', function () { + const bidCopy = { ...bid }; + bidCopy.params = { + ...bid.params, + pageContext: { + dv: 'test/areatest', + du: 'https://example.com/', + dr: 'https://example.org/', + keyWords: ['val1', 'val2'], + keyValues: { + adunit: 'test/areatest' + } + } + }; + let bidderRequest = { + ortb2: { + regs: { + ext: { + dsa: { + required: 1 + } + } + } + } + }; + let requests = spec.buildRequests([bidCopy], bidderRequest); + expect(requests[0].url).to.have.string('dsainfo=1'); + + bidderRequest.ortb2.regs.ext.dsa.required = 0; + requests = spec.buildRequests([bidCopy], bidderRequest); + expect(requests[0].url).to.have.string('dsainfo=0'); + }); }); describe('interpretResponse', function () { @@ -171,7 +229,7 @@ describe('rasBidAdapter', function () { it('should get correct bid response', function () { const resp = spec.interpretResponse({ body: response }, { bidIds: [{ slot: 'flat-belkagorna', bidId: 1 }] }); - expect(resp[0]).to.have.all.keys('cpm', 'currency', 'netRevenue', 'requestId', 'ttl', 'width', 'height', 'creativeId', 'dealId', 'ad', 'meta'); + expect(resp[0]).to.have.all.keys('cpm', 'currency', 'netRevenue', 'requestId', 'ttl', 'width', 'height', 'creativeId', 'dealId', 'ad', 'meta', 'actgMatch', 'mediaType'); expect(resp.length).to.equal(1); }); @@ -192,5 +250,361 @@ describe('rasBidAdapter', function () { const resp = spec.interpretResponse({ body: res }, {}); expect(resp).to.deep.equal([]); }); + + it('should generate auctionConfig when fledge is enabled', function () { + let bidRequest = { + method: 'GET', + url: 'https://example.com', + bidIds: [{ + slot: 'top', + bidId: '123', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: true + }, + { + slot: 'top', + bidId: '456', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: false + }] + }; + + let auctionConfigs = [{ + 'bidId': '123', + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': 'https://csr.onet.pl/testnetwork/v1/protected-audience-api/decision-logic.js', + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + 'sizes': ['300x250'], + 'gctx': '1234567890' + } + } + }]; + const resp = spec.interpretResponse({body: {gctx: '1234567890'}}, bidRequest); + expect(resp).to.deep.equal({bids: [], fledgeAuctionConfigs: auctionConfigs}); + }); + }); + + describe('buildNativeRequests', function () { + const bid = { + sizes: 'fluid', + bidder: 'ras', + bidId: 1, + params: { + slot: 'nativestd', + area: 'areatest', + site: 'test', + slotSequence: '0', + network: '4178463', + customParams: { + test: 'name=value' + } + }, + mediaTypes: { + native: { + clickUrl: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + len: 25, + required: true + }, + title: { + len: 50, + required: true + } + } + } + }; + + it('should parse bids to native request', function () { + const requests = spec.buildRequests([bid], { + 'gdprConsent': { + 'gdprApplies': true, + 'consentString': 'some-consent-string' + }, + 'refererInfo': { + 'ref': 'https://example.org/', + 'page': 'https://example.com/' + } + }); + + expect(requests[0].url).to.have.string(CSR_ENDPOINT); + expect(requests[0].url).to.have.string('slot0=nativestd'); + expect(requests[0].url).to.have.string('id0=1'); + expect(requests[0].url).to.have.string('site=test'); + expect(requests[0].url).to.have.string('area=areatest'); + expect(requests[0].url).to.have.string('cre_format=html'); + expect(requests[0].url).to.have.string('systems=das'); + expect(requests[0].url).to.have.string('ems_url=1'); + expect(requests[0].url).to.have.string('bid_rate=1'); + expect(requests[0].url).to.have.string('gdpr_applies=true'); + expect(requests[0].url).to.have.string('euconsent=some-consent-string'); + expect(requests[0].url).to.have.string('du=https%3A%2F%2Fexample.com%2F'); + expect(requests[0].url).to.have.string('dr=https%3A%2F%2Fexample.org%2F'); + expect(requests[0].url).to.have.string('test=name%3Dvalue'); + expect(requests[0].url).to.have.string('cre_format0=native'); + expect(requests[0].url).to.have.string('iusizes0=fluid'); + }); + }); + + describe('interpretNativeResponse', function () { + const response = { + 'adsCheck': 'ok', + 'geoloc': {}, + 'ir': '92effd60-0c84-4dac-817e-763ea7b8ac65', + 'iv': '202003191334467636346500', + 'ads': [ + { + 'id': 'nativestd', + 'slot': 'nativestd', + 'prio': 10, + 'type': 'native', + 'bid_rate': 0.321123, + 'adid': 'das,50463,152276', + 'id_3': '12734' + } + ] + }; + const responseTeaserStandard = { + adsCheck: 'ok', + geoloc: {}, + ir: '92effd60-0c84-4dac-817e-763ea7b8ac65', + iv: '202003191334467636346500', + ads: [ + { + id: 'nativestd', + slot: 'nativestd', + prio: 10, + type: 'native', + bid_rate: 0.321123, + adid: 'das,50463,152276', + id_3: '12734', + data: { + fields: { + leadtext: 'BODY', + title: 'Headline', + image: '//img.url', + url: '//link.url', + impression: '//impression.url', + impression1: '//impression1.url', + impressionJs1: '//impressionJs1.url' + }, + meta: { + slot: 'nativestd', + height: 1, + width: 1, + advertiser_name: 'Test Onet', + dsaurl: '//dsa.url', + adclick: '//adclick.url' + } + }, + ems_link: '//ems.url' + } + ] + }; + const responseNativeInFeed = { + adsCheck: 'ok', + geoloc: {}, + ir: '92effd60-0c84-4dac-817e-763ea7b8ac65', + iv: '202003191334467636346500', + ads: [ + { + id: 'nativestd', + slot: 'nativestd', + prio: 10, + type: 'native', + bid_rate: 0.321123, + adid: 'das,50463,152276', + id_3: '12734', + data: { + fields: { + Body: 'BODY', + Calltoaction: 'Calltoaction', + Headline: 'Headline', + Image: '//img.url', + Sponsorlabel: 'nie', + Thirdpartyclicktracker: '//link.url', + imp: '//imp.url' + }, + meta: { + slot: 'nativestd', + height: 1, + width: 1, + advertiser_name: 'Test Onet', + dsaurl: '//dsa.url', + adclick: '//adclick.url' + } + }, + ems_link: '//ems.url' + } + ] + }; + const expectedTeaserStandardOrtbResponse = { + ver: '1.2', + assets: [ + { + id: 2, + img: { + url: '//img.url', + w: 1, + h: 1 + } + }, + { + id: 4, + title: { + text: 'Headline' + } + }, + { + id: 3, + data: { + value: 'Test Onet', + type: 1 + } + } + ], + link: { + url: '//adclick.url//link.url' + }, + eventtrackers: [ + { + event: 1, + method: 1, + url: '//ems.url' + }, + { + event: 1, + method: 1, + url: '//impression.url' + }, + { + event: 1, + method: 1, + url: '//impression1.url' + }, + { + event: 1, + method: 2, + url: '//impressionJs1.url' + } + ], + privacy: '//dsa.url' + }; + const expectedTeaserStandardResponse = { + sendTargetingKeys: false, + title: 'Headline', + image: { + url: '//img.url', + width: 1, + height: 1 + }, + clickUrl: '//adclick.url//link.url', + cta: '', + body: 'BODY', + sponsoredBy: 'Test Onet', + ortb: expectedTeaserStandardOrtbResponse, + privacyLink: '//dsa.url' + }; + const expectedNativeInFeedOrtbResponse = { + ver: '1.2', + assets: [ + { + id: 2, + img: { + url: '//img.url', + w: 1, + h: 1 + } + }, + { + id: 4, + title: { + text: 'Headline' + } + }, + { + id: 3, + data: { + value: 'Test Onet', + type: 1 + } + } + ], + link: { + url: '//adclick.url//link.url' + }, + eventtrackers: [ + { + event: 1, + method: 1, + url: '//ems.url' + }, + { + event: 1, + method: 1, + url: '//imp.url' + } + ], + privacy: '//dsa.url', + }; + const expectedNativeInFeedResponse = { + sendTargetingKeys: false, + title: 'Headline', + image: { + url: '//img.url', + width: 1, + height: 1 + }, + clickUrl: '//adclick.url//link.url', + cta: 'Calltoaction', + body: 'BODY', + sponsoredBy: 'Test Onet', + ortb: expectedNativeInFeedOrtbResponse, + privacyLink: '//dsa.url' + }; + + it('should get correct bid native response', function () { + const resp = spec.interpretResponse({ body: response }, { bidIds: [{ slot: 'nativestd', bidId: 1, mediaType: 'native' }] }); + + expect(resp[0]).to.have.all.keys('cpm', 'currency', 'netRevenue', 'requestId', 'ttl', 'width', 'height', 'creativeId', 'dealId', 'meta', 'actgMatch', 'mediaType', 'native'); + expect(resp.length).to.equal(1); + }); + + it('should get correct native response for TeaserStandard', function () { + const resp = spec.interpretResponse({ body: responseTeaserStandard }, { bidIds: [{ slot: 'nativestd', bidId: 1, mediaType: 'native' }] }); + const teaserStandardResponse = resp[0].native; + + expect(JSON.stringify(teaserStandardResponse)).to.equal(JSON.stringify(expectedTeaserStandardResponse)); + }); + + it('should get correct native response for NativeInFeed', function () { + const resp = spec.interpretResponse({ body: responseNativeInFeed }, { bidIds: [{ slot: 'nativestd', bidId: 1, mediaType: 'native' }] }); + const nativeInFeedResponse = resp[0].native; + + expect(JSON.stringify(nativeInFeedResponse)).to.equal(JSON.stringify(expectedNativeInFeedResponse)); + }); }); }); diff --git a/test/spec/modules/raynRtdProvider_spec.js b/test/spec/modules/raynRtdProvider_spec.js new file mode 100644 index 00000000000..69ea316e8b5 --- /dev/null +++ b/test/spec/modules/raynRtdProvider_spec.js @@ -0,0 +1,308 @@ +import * as raynRTD from 'modules/raynRtdProvider.js'; +import { config } from 'src/config.js'; +import * as utils from 'src/utils.js'; + +const TEST_CHECKSUM = '-1135402174'; +const TEST_URL = 'http://localhost:9876/context.html'; +const TEST_SEGMENTS = { + [TEST_CHECKSUM]: { + 7: { + 2: ['51', '246', '652', '48', '324'] + } + } +}; + +const RTD_CONFIG = { + auctionDelay: 250, + dataProviders: [ + { + name: 'rayn', + waitForIt: true, + params: { + bidders: [], + integration: { + iabAudienceCategories: { + v1_1: { + tier: 6, + enabled: true, + }, + }, + iabContentCategories: { + v3_0: { + tier: 4, + enabled: true, + }, + v2_2: { + tier: 4, + enabled: true, + }, + }, + } + }, + }, + ], +}; + +describe('rayn RTD Submodule', function () { + let getDataFromLocalStorageStub; + + beforeEach(function () { + config.resetConfig(); + getDataFromLocalStorageStub = sinon.stub( + raynRTD.storage, + 'getDataFromLocalStorage', + ); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + describe('Initialize module', function () { + it('should initialize and return true', function () { + expect(raynRTD.raynSubmodule.init(RTD_CONFIG.dataProviders[0])).to.equal( + true, + ); + }); + }); + + describe('Generate ortb data object', function () { + it('should set empty segment array', function () { + expect(raynRTD.generateOrtbDataObject(7, 'invalid', 2).segment).to.be.instanceOf(Array).and.lengthOf(0); + }); + + it('should set segment array', function () { + const expectedSegmentIdsMap = TEST_SEGMENTS[TEST_CHECKSUM][7][2].map((id) => { + return { id }; + }); + expect(raynRTD.generateOrtbDataObject(7, TEST_SEGMENTS[TEST_CHECKSUM][7], 4)).to.deep.equal({ + name: raynRTD.SEGMENTS_RESOLVER, + ext: { + segtax: 7, + }, + segment: expectedSegmentIdsMap, + }); + }); + }); + + describe('Generate checksum', function () { + it('should generate checksum', function () { + expect(raynRTD.generateChecksum(TEST_URL)).to.equal(TEST_CHECKSUM); + }); + }); + + describe('Get segments', function () { + it('should get segments from local storage', function () { + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(JSON.stringify(TEST_SEGMENTS)); + + const segments = raynRTD.readSegments(raynRTD.RAYN_LOCAL_STORAGE_KEY); + + expect(segments).to.deep.equal(TEST_SEGMENTS); + }); + + it('should return null if unable to read and parse data from local storage', function () { + const testString = 'test'; + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(testString); + + const segments = raynRTD.readSegments(raynRTD.RAYN_LOCAL_STORAGE_KEY); + + expect(segments).to.equal(null); + }); + }); + + describe('Set segments as bidder ortb2', function () { + it('should set global ortb2 config', function () { + const globalOrtb2 = {}; + const bidders = RTD_CONFIG.dataProviders[0].params.bidders; + const integrationConfig = RTD_CONFIG.dataProviders[0].params.integration; + + raynRTD.setSegmentsAsBidderOrtb2({ ortb2Fragments: { global: globalOrtb2 } }, bidders, integrationConfig, TEST_SEGMENTS, TEST_CHECKSUM); + + TEST_SEGMENTS[TEST_CHECKSUM]['7']['2'].forEach((id) => { + expect(globalOrtb2.site.content.data[0].segment.find(segment => segment.id === id)).to.exist; + }) + }); + + it('should set bidder specific ortb2 config', function () { + RTD_CONFIG.dataProviders[0].params.bidders = ['appnexus']; + + const bidderOrtb2 = {}; + const bidders = RTD_CONFIG.dataProviders[0].params.bidders; + const integrationConfig = RTD_CONFIG.dataProviders[0].params.integration; + + raynRTD.setSegmentsAsBidderOrtb2({ ortb2Fragments: { bidder: bidderOrtb2 } }, bidders, integrationConfig, TEST_SEGMENTS, TEST_CHECKSUM); + + bidders.forEach((bidder) => { + const ortb2 = bidderOrtb2[bidder]; + TEST_SEGMENTS[TEST_CHECKSUM]['7']['2'].forEach((id) => { + expect(ortb2.site.content.data[0].segment.find(segment => segment.id === id)).to.exist; + }) + }); + }); + + it('should set bidder specific ortb2 config with all segments', function () { + TEST_SEGMENTS['4'] = { + 3: ['4', '17', '72', '612'] + }; + TEST_SEGMENTS[TEST_CHECKSUM]['6'] = { + 2: ['71', '313'], + 4: ['33', '145', '712'] + }; + + const bidderOrtb2 = {}; + const bidders = RTD_CONFIG.dataProviders[0].params.bidders; + const integrationConfig = RTD_CONFIG.dataProviders[0].params.integration; + + raynRTD.setSegmentsAsBidderOrtb2({ ortb2Fragments: { bidder: bidderOrtb2 } }, bidders, integrationConfig, TEST_SEGMENTS, TEST_CHECKSUM); + + bidders.forEach((bidder) => { + const ortb2 = bidderOrtb2[bidder]; + + TEST_SEGMENTS[TEST_CHECKSUM]['6']['2'].forEach((id) => { + expect(ortb2.site.content.data[0].segment.find(segment => segment.id === id)).to.exist; + }); + TEST_SEGMENTS[TEST_CHECKSUM]['6']['4'].forEach((id) => { + expect(ortb2.site.content.data[0].segment.find(segment => segment.id === id)).to.exist; + }); + TEST_SEGMENTS[TEST_CHECKSUM]['7']['2'].forEach((id) => { + expect(ortb2.site.content.data[1].segment.find(segment => segment.id === id)).to.exist; + }); + TEST_SEGMENTS['4']['3'].forEach((id) => { + expect(ortb2.user.data[0].segment.find(segment => segment.id === id)).to.exist; + }); + }); + }); + }); + + describe('Alter Bid Requests', function () { + it('should update reqBidsConfigObj and execute callback', function () { + const callbackSpy = sinon.spy(); + const logMessageSpy = sinon.spy(utils, 'logMessage'); + + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(JSON.stringify(TEST_SEGMENTS)); + + const reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + + raynRTD.raynSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, RTD_CONFIG); + + expect(callbackSpy.calledOnce).to.be.true; + expect(logMessageSpy.lastCall.lastArg).to.equal(`Segtax data from localStorage: ${JSON.stringify(TEST_SEGMENTS)}`); + + logMessageSpy.restore(); + }); + + it('should update reqBidsConfigObj and execute callback using user segments from localStorage', function () { + const callbackSpy = sinon.spy(); + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const testSegments = { + 4: { + 3: ['4', '17', '72', '612'] + } + }; + + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(JSON.stringify(testSegments)); + + RTD_CONFIG.dataProviders[0].params.integration.iabContentCategories = { + v3_0: { + enabled: false, + }, + v2_2: { + enabled: false, + }, + }; + + const reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + + raynRTD.raynSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, RTD_CONFIG.dataProviders[0]); + + expect(callbackSpy.calledOnce).to.be.true; + expect(logMessageSpy.lastCall.lastArg).to.equal(`Segtax data from localStorage: ${JSON.stringify(testSegments)}`); + + logMessageSpy.restore(); + }); + + it('should update reqBidsConfigObj and execute callback using segments from raynJS', function () { + const callbackSpy = sinon.spy(); + const logMessageSpy = sinon.spy(utils, 'logMessage'); + + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(null); + + const reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + + raynRTD.raynSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, RTD_CONFIG.dataProviders[0]); + + expect(callbackSpy.calledOnce).to.be.true; + expect(logMessageSpy.lastCall.lastArg).to.equal(`No segtax data`); + + logMessageSpy.restore(); + }); + + it('should update reqBidsConfigObj and execute callback using audience from localStorage', function (done) { + const callbackSpy = sinon.spy(); + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const testSegments = { + 6: { + 4: ['3', '27', '177'] + } + }; + + global.window.raynJS = { + getSegtax: function () { + return Promise.resolve(testSegments); + } + }; + + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(null); + + const reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + + raynRTD.raynSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, RTD_CONFIG.dataProviders[0]); + + setTimeout(() => { + expect(callbackSpy.calledOnce).to.be.true; + expect(logMessageSpy.lastCall.lastArg).to.equal(`Segtax data from RaynJS: ${JSON.stringify(testSegments)}`); + logMessageSpy.restore(); + done(); + }, 0) + }); + + it('should execute callback if log error', function (done) { + const callbackSpy = sinon.spy(); + const logErrorSpy = sinon.spy(utils, 'logError'); + const rejectError = 'Error'; + + global.window.raynJS = { + getSegtax: function () { + return Promise.reject(rejectError); + } + }; + + getDataFromLocalStorageStub + .withArgs(raynRTD.RAYN_LOCAL_STORAGE_KEY) + .returns(null); + + const reqBidsConfigObj = { ortb2Fragments: { bidder: {} } }; + + raynRTD.raynSubmodule.getBidRequestData(reqBidsConfigObj, callbackSpy, RTD_CONFIG.dataProviders[0]); + + setTimeout(() => { + expect(callbackSpy.calledOnce).to.be.true; + expect(logErrorSpy.lastCall.lastArg).to.equal(rejectError); + logErrorSpy.restore(); + done(); + }, 0) + }); + }); +}); diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index 8772aeac88f..32a4d991054 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -376,7 +376,7 @@ describe('ReadPeakAdapter', function() { height: 500 }); expect(bidResponse.native.clickUrl).to.equal( - 'http%3A%2F%2Furl.to%2Ftarget' + 'http://url.to/target' ); expect(bidResponse.native.impressionTrackers).to.contain( 'http://url.to/pixeltracker' diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 7778e9cbf80..f0d019913e8 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -239,6 +239,7 @@ describe('RelaidoAdapter', function () { const request = data.bids[0]; expect(bidRequests.method).to.equal('POST'); expect(bidRequests.url).to.equal('https://api.relaido.jp/bid/v1/sprebid'); + expect(data.canonical_url).to.equal('https://publisher.com/home'); expect(data.canonical_url_hash).to.equal('e6092f44a0044903ae3764126eedd6187c1d9f04'); expect(data.ref).to.equal(bidderRequest.refererInfo.page); expect(data.timeout_ms).to.equal(bidderRequest.timeout); @@ -317,6 +318,23 @@ describe('RelaidoAdapter', function () { expect(data.bids).to.have.lengthOf(1); expect(data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); }); + + it('should get userIdAsEids', function () { + const userIdAsEids = [ + { + source: 'hogehoge.com', + uids: { + atype: 1, + id: 'hugahuga' + } + } + ] + bidRequest.userIdAsEids = userIdAsEids + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + const data = JSON.parse(bidRequests.data); + expect(data.bids[0].userIdAsEids).to.have.lengthOf(1); + expect(data.bids[0].userIdAsEids[0].source).to.equal('hogehoge.com'); + }); }); describe('spec.interpretResponse', function () { @@ -325,6 +343,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponse.body.ads[0].placementId); expect(response.width).to.equal(serverRequest.data.bids[0].width); expect(response.height).to.equal(serverRequest.data.bids[0].height); expect(response.cpm).to.equal(serverResponse.body.ads[0].price); @@ -343,6 +362,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponse.body.ads[0].placementId); expect(response.width).to.equal(serverRequest.data.bids[0].width); expect(response.height).to.equal(serverRequest.data.bids[0].height); expect(response.cpm).to.equal(serverResponse.body.ads[0].price); @@ -360,6 +380,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponseBanner.body.ads[0].placementId); expect(response.cpm).to.equal(serverResponseBanner.body.ads[0].price); expect(response.currency).to.equal(serverResponseBanner.body.ads[0].currency); expect(response.creativeId).to.equal(serverResponseBanner.body.ads[0].creativeId); diff --git a/test/spec/modules/relayBidAdapter_spec.js b/test/spec/modules/relayBidAdapter_spec.js new file mode 100644 index 00000000000..38a3cfc9b97 --- /dev/null +++ b/test/spec/modules/relayBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/relayBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'relay' +const endpoint = 'https://e.relay.bid/p/openrtb2'; + +describe('RelayBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 15000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'abc123' + }, + bidderB: { + theId: 'xyz789' + } + } + } + } + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 30000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'def456' + }, + bidderB: { + theId: 'uvw101112' + } + } + } + } + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: {} + } + + const bidderRequest = {}; + + describe('isBidRequestValid', function () { + it('Valid bids have a params.accountId.', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Invalid bids do not have a params.accountId.', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + const requests = spec.buildRequests(bids, bidderRequest); + const firstRequest = requests[0]; + const secondRequest = requests[1]; + + it('Creates two requests', function () { + expect(firstRequest).to.exist; + expect(firstRequest.data).to.exist; + expect(firstRequest.method).to.exist; + expect(firstRequest.method).to.equal('POST'); + expect(firstRequest.url).to.exist; + expect(firstRequest.url).to.equal(`${endpoint}?a=15000&pb=1&pbv=v8.1.0`); + + expect(secondRequest).to.exist; + expect(secondRequest.data).to.exist; + expect(secondRequest.method).to.exist; + expect(secondRequest.method).to.equal('POST'); + expect(secondRequest.url).to.exist; + expect(secondRequest.url).to.equal(`${endpoint}?a=30000&pb=1&pbv=v8.1.0`); + }); + + it('Does not generate requests when there are no bids', function () { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Uses Prebid consent values if incoming sync URLs lack consent.', function () { + const syncOpts = { + iframeEnabled: true, + pixelEnabled: true + }; + const test_gdpr_applies = true; + const test_gdpr_consent_str = 'TEST_GDPR_CONSENT_STRING'; + const responses = [{ + body: { + ext: { + user_syncs: [ + { type: 'image', url: 'https://image-example.com' }, + { type: 'iframe', url: 'https://iframe-example.com' } + ] + } + } + }]; + + const sync_urls = spec.getUserSyncs(syncOpts, responses, { gdprApplies: test_gdpr_applies, consentString: test_gdpr_consent_str }); + expect(sync_urls).to.be.an('array'); + expect(sync_urls[0].url).to.equal('https://image-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + expect(sync_urls[1].url).to.equal('https://iframe-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + }); + }); +}); diff --git a/test/spec/modules/relevantdigitalBidAdapter_spec.js b/test/spec/modules/relevantdigitalBidAdapter_spec.js index b2a5495b3cb..0e21453c8ba 100644 --- a/test/spec/modules/relevantdigitalBidAdapter_spec.js +++ b/test/spec/modules/relevantdigitalBidAdapter_spec.js @@ -1,5 +1,10 @@ import {spec, resetBidderConfigs} from 'modules/relevantdigitalBidAdapter.js'; import { parseUrl, deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; +import CONSTANTS from 'src/constants.json'; + +import adapterManager, { +} from 'src/adapterManager.js'; const expect = require('chai').expect; @@ -9,14 +14,29 @@ const ACCOUNT_ID = 'example_account_id'; const TEST_DOMAIN = 'example.com'; const TEST_PAGE = `https://${TEST_DOMAIN}/page.html`; -const BID_REQUEST = -{ - 'bidder': 'relevantdigital', +const CONFIG = { + enabled: true, + endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT, + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer', + bidders: ['relevantdigital'], + accountId: 'abc' +}; + +const ADUNIT_CODE = '/19968336/header-bid-tag-0'; + +const BID_PARAMS = { 'params': { 'placementId': PLACEMENT_ID, 'accountId': ACCOUNT_ID, - 'pbsHost': PBS_HOST, - }, + 'pbsHost': PBS_HOST + } +}; + +const BID_REQUEST = { + 'bidder': 'relevantdigital', + ...BID_PARAMS, 'ortb2Imp': { 'ext': { 'tid': 'e13391ea-00f3-495d-99a6-d937990d73a9' @@ -32,7 +52,7 @@ const BID_REQUEST = ] } }, - 'adUnitCode': '/19968336/header-bid-tag-0', + 'adUnitCode': ADUNIT_CODE, 'transactionId': 'e13391ea-00f3-495d-99a6-d937990d73a9', 'sizes': [ [ @@ -292,4 +312,64 @@ describe('Relevant Digital Bid Adaper', function () { expect(allSyncs).to.deep.equal(expectedResult) }); }); + describe('transformBidParams', function () { + beforeEach(() => { + config.setConfig({ + s2sConfig: CONFIG, + }); + }); + afterEach(() => { + config.resetConfig(); + }); + + const adUnit = (params) => ({ + code: ADUNIT_CODE, + bids: [ + { + bidder: 'relevantdigital', + adUnitCode: ADUNIT_CODE, + params, + } + ] + }); + + const request = (params) => adapterManager.makeBidRequests([adUnit(params)], 123, 'auction-id', 123, [], {})[0]; + + it('transforms adunit bid params and config params correctly', function () { + config.setConfig({ + relevantdigital: { + pbsHost: PBS_HOST, + accountId: ACCOUNT_ID, + }, + }); + const adUnitParams = { placementId: PLACEMENT_ID }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: `https://${BID_PARAMS.params.pbsHost}`, 'pbsBufferMs': 250 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('does not transform bid params if placementId is missing', function () { + const adUnitParams = { ...BID_PARAMS.params, placementId: null }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + it('does not transform bid params s2s config is missing', function () { + config.resetConfig(); + const adUnitParams = BID_PARAMS.params; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + }) }); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index ea45ff7e0b0..d2b173f53df 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -4,6 +4,8 @@ import { spec } from 'modules/richaudienceBidAdapter.js'; import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import sinon from 'sinon'; describe('Richaudience adapter tests', function () { var DEFAULT_PARAMS_NEW_SIZES = [{ @@ -64,6 +66,30 @@ describe('Richaudience adapter tests', function () { user: {} }]; + var DEFAULT_PARAMS_VIDEO_TIMEOUT = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'richaudience', + params: [{ + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site' + }], + timeout: 3000, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }] + var DEFAULT_PARAMS_VIDEO_IN = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', @@ -267,7 +293,7 @@ describe('Richaudience adapter tests', function () { expect(requestContent.sizes[3]).to.have.property('w').and.to.equal(970); expect(requestContent.sizes[3]).to.have.property('h').and.to.equal(250); expect(requestContent).to.have.property('transactionId').and.to.equal('29df2112-348b-4961-8863-1b33684d95e6'); - expect(requestContent).to.have.property('timeout').and.to.equal(3000); + expect(requestContent).to.have.property('timeout').and.to.equal(600); expect(requestContent).to.have.property('numIframes').and.to.equal(0); expect(typeof requestContent.scr_rsl === 'string') expect(typeof requestContent.cpuc === 'number') @@ -879,7 +905,32 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('gpid').and.to.equal('/19968336/header-bid-tag-1#example-2'); }) + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('onTimeout exist as a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should send timeouts', function () { + spec.onTimeout(DEFAULT_PARAMS_VIDEO_TIMEOUT); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.firstCall.args[0]).to.equal('https://s.richaudience.com/err/?ec=6&ev=3000&pla=ADb1f40rmi&int=PREBID&pltfm=&node=&dm=localhost:9876'); + }); + }); + describe('userSync', function () { + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + afterEach(function() { + sandbox.restore(); + }); it('Verifies user syncs iframe include', function () { config.setConfig({ 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}}} @@ -1217,5 +1268,37 @@ describe('Richaudience adapter tests', function () { }, [], {consentString: '', gdprApplies: true}); expect(syncs).to.have.lengthOf(0); }); + + it('Verifies user syncs iframe/image include with GPP', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({iframeEnabled: true}, [BID_RESPONSE], { + gppString: 'DBABL~BVVqAAEABgA.QA', + applicableSections: [7]}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + + config.setConfig({ + 'userSync': {filterSettings: {image: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({pixelEnabled: true}, [BID_RESPONSE], { + gppString: 'DBABL~BVVqAAEABgA.QA', + applicableSections: [7, 5]}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + }); + + it('Verifies user syncs URL image include with GPP', function () { + const gppConsent = { gppString: 'DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA', applicableSections: [0] }; + const result = spec.getUserSyncs({pixelEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'image', url: `https://sync.richaudience.com/bf7c142f4339da0278e83698a02b0854/?referrer=http%3A%2F%2Fdomain.com&gpp=DBACMYA~CP5P4cAP5P4cAPoABAESAlEAAAAAAAAAAAAAA2QAQA2ADZABADYAAAAA.QA2QAQA2AAAA.IA2QAQA2AAAA~BP5P4cAP5P4cAPoABABGBACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA&gpp_sid=0` + }]); + }); }) }); diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index eed8d74f271..ec9309fd4ae 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -22,6 +22,12 @@ describe('riseAdapter', function () { }); }); + describe('bid adapter', function () { + it('should have aliases', function () { + expect(spec.aliases).to.be.an('array').that.is.not.empty; + }); + }); + describe('isBidRequestValid', function () { const bid = { 'bidder': spec.code, @@ -53,7 +59,7 @@ describe('riseAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'org': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001', }, 'bidId': '299ffc8cca0b87', 'loop': 1, @@ -195,6 +201,16 @@ describe('riseAdapter', function () { expect(request.data.bids[1].mediaType).to.equal(BANNER) }); + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + it('should respect syncEnabled option', function() { config.setConfig({ userSync: { @@ -308,6 +324,24 @@ describe('riseAdapter', function () { expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); }); + it('should not send the gpp param if gppConsent is false in the bidRequest', function () { + const bidderRequestWithoutGPP = Object.assign({gppConsent: false}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithoutGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gpp'); + expect(request.data.params).to.not.have.property('gpp_sid'); + }); + + it('should send the gpp param if gppConsent is true in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: {gppString: 'gpp-consent', applicableSections: [7]}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + console.log('request.data.params'); + console.log(request.data.params); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gpp', 'gpp-consent'); + expect(request.data.params.gpp_sid[0]).to.be.equal(7); + }); + it('should have schain param if it is available in the bidRequest', () => { const schain = { ver: '1.0', diff --git a/test/spec/modules/rixengineBidAdapter_spec.js b/test/spec/modules/rixengineBidAdapter_spec.js new file mode 100644 index 00000000000..a400b5c755b --- /dev/null +++ b/test/spec/modules/rixengineBidAdapter_spec.js @@ -0,0 +1,141 @@ +import { spec } from 'modules/rixengineBidAdapter.js'; +const ENDPOINT = 'http://demo.svr.rixengine.com/rtb?sid=36540&token=1e05a767930d7d96ef6ce16318b4ab99'; + +const REQUEST = [ + { + adUnitCode: 'adUnitCode1', + bidId: 'bidId1', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + mediaTypes: { + banner: {}, + }, + bidder: 'rixengine', + params: { + endpoint: 'http://demo.svr.rixengine.com/rtb', + token: '1e05a767930d7d96ef6ce16318b4ab99', + sid: '36540', + }, + }, +]; + +const RESPONSE = { + headers: null, + body: { + id: 'requestId', + bidid: 'bidId1', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: 'bidId1', + impid: 'bidId1', + adm: '', + cid: '24:17:18', + crid: '40_37_66:30_32_132:31_27_70', + adomain: ['www.rixengine.com'], + price: 10.00, + bundle: + 'com.xinggame.cast.video.screenmirroring.casttotv:https://www.greysa.com.tw/Product/detail/pid/119/?utm_source=popIn&utm_medium=cpc&utm_campaign=neck_202307_300*250:https://www.avaige.top/', + iurl: 'https://crs.rixbeedesk.com/test/kkd2ms/04c6d62912cff9037106fb50ed21b558.png:https://crs.rixbeedesk.com/test/kkd2ms/69a72b23c6c52e703c0c8e3f634e44eb.png:https://crs.rixbeedesk.com/test/kkd2ms/d229c5cd66bcc5856cb26bb2817718c9.png', + w: 300, + h: 250, + exp: 30, + }, + ], + seat: 'Zh2Kiyk=', + }, + ], + }, +}; + +describe('rixengine bid adapter', function () { + describe('isBidRequestValid', function () { + let bid = { + bidder: 'rixengine', + params: { + endpoint: 'http://demo.svr.rixengine.com/rtb', + token: '1e05a767930d7d96ef6ce16318b4ab99', + sid: '36540', + }, + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when missing endpoint', function () { + delete bid.params.endpoint; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when missing sid', function () { + delete bid.params.sid; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when missing token', function () { + delete bid.params.token; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + describe('buildRequests', function () { + it('creates request data', function () { + const request = spec.buildRequests(REQUEST, { + refererInfo: { + page: 'page', + }, + })[0]; + expect(request).to.exist.and.to.be.a('object'); + }); + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(REQUEST, {})[0]; + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + }); + + describe('interpretResponse', function () { + it('has bids', function () { + let request = spec.buildRequests(REQUEST, {})[0]; + let bids = spec.interpretResponse(RESPONSE, request); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property( + 'requestId', + RESPONSE.body.seatbid[0].bid[index].id + ); + expect(bids[index]).to.have.property( + 'cpm', + RESPONSE.body.seatbid[0].bid[index].price + ); + expect(bids[index]).to.have.property( + 'width', + RESPONSE.body.seatbid[0].bid[index].w + ); + expect(bids[index]).to.have.property( + 'height', + RESPONSE.body.seatbid[0].bid[index].h + ); + expect(bids[index]).to.have.property( + 'ad', + RESPONSE.body.seatbid[0].bid[index].adm + ); + expect(bids[index]).to.have.property( + 'creativeId', + RESPONSE.body.seatbid[0].bid[index].crid + ); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + it('No bid response', function() { + var noBidResponse = spec.interpretResponse({ + body: '', + }); + expect(noBidResponse.length).to.equal(0); + }); + }); + }); +}); diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 0b944dcb077..77b746b9b69 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { OPENRTB, spec } from 'modules/rtbhouseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { mergeDeep } from '../../../src/utils'; describe('RTBHouseAdapter', () => { const adapter = newBidder(spec); @@ -304,6 +305,152 @@ describe('RTBHouseAdapter', () => { expect(data.user).to.nested.include({'ext.data': 'some user data'}); }); + context('DSA', () => { + const validDSAObject = { + 'dsarequired': 3, + 'pubrender': 0, + 'datatopub': 2, + 'transparency': [ + { + 'domain': 'platform1domain.com', + 'dsaparams': [1] + }, + { + 'domain': 'SSP2domain.com', + 'dsaparams': [1, 2] + } + ] + }; + const invalidDSAObjects = [ + -1, + 0, + '', + 'x', + true, + [], + [1], + {}, + { + 'dsarequired': -1 + }, + { + 'pubrender': -1 + }, + { + 'datatopub': -1 + }, + { + 'dsarequired': 4 + }, + { + 'pubrender': 3 + }, + { + 'datatopub': 3 + }, + { + 'dsarequired': '1' + }, + { + 'pubrender': '1' + }, + { + 'datatopub': '1' + }, + { + 'transparency': '1' + }, + { + 'transparency': 2 + }, + { + 'transparency': [ + 1, 2 + ] + }, + { + 'transparency': [ + { + domain: '', + dsaparams: [] + } + ] + }, + { + 'transparency': [ + { + domain: 'x', + dsaparams: null + } + ] + }, + { + 'transparency': [ + { + domain: 'x', + dsaparams: [1, '2'] + } + ] + }, + ]; + let bidRequest; + + beforeEach(() => { + bidRequest = Object.assign([], bidRequests); + }); + + it('should add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa', function () { + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + ext: { + dsa: validDSAObject + } + } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + + expect(data).to.have.nested.property('regs.ext.dsa'); + expect(data.regs.ext.dsa.dsarequired).to.equal(3); + expect(data.regs.ext.dsa.pubrender).to.equal(0); + expect(data.regs.ext.dsa.datatopub).to.equal(2); + expect(data.regs.ext.dsa.transparency).to.deep.equal([ + { + 'domain': 'platform1domain.com', + 'dsaparams': [1] + }, + { + 'domain': 'SSP2domain.com', + 'dsaparams': [1, 2] + } + ]); + }); + + invalidDSAObjects.forEach((invalidDSA, index) => { + it(`should not add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa; test# ${index}`, function () { + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + ext: { + dsa: invalidDSA + } + } + } + }; + + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + + expect(data).to.not.have.nested.property('regs.ext.dsa'); + }); + }); + }); + context('FLEDGE', function() { afterEach(function () { config.resetConfig(); @@ -563,17 +710,20 @@ describe('RTBHouseAdapter', () => { }); describe('interpretResponse', function () { - let response = [{ - 'id': 'bidder_imp_identifier', - 'impid': '552b8922e28f27', - 'price': 0.5, - 'adid': 'Ad_Identifier', - 'adm': '', - 'adomain': ['rtbhouse.com'], - 'cid': 'Ad_Identifier', - 'w': 300, - 'h': 250 - }]; + let response; + beforeEach(() => { + response = [{ + 'id': 'bidder_imp_identifier', + 'impid': '552b8922e28f27', + 'price': 0.5, + 'adid': 'Ad_Identifier', + 'adm': '', + 'adomain': ['rtbhouse.com'], + 'cid': 'Ad_Identifier', + 'w': 300, + 'h': 250 + }]; + }); let fledgeResponse = { 'id': 'bid-identifier', @@ -638,6 +788,51 @@ describe('RTBHouseAdapter', () => { }); }); + context('when the response contains DSA object', function () { + it('should get correct bid response', function () { + const dsa = { + 'dsa': { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': [{ + 'domain': 'dsp1domain.com', + 'dsaparams': [1, 2] + }], + 'adrender': 1 + } + }; + mergeDeep(response[0], { ext: dsa }); + + const expectedResponse = [ + { + 'requestId': '552b8922e28f27', + 'cpm': 0.5, + 'creativeId': 29681110, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'meta': { + 'advertiserDomains': ['rtbhouse.com'], + ...dsa + }, + 'netRevenue': true, + ext: { ...dsa } + } + ]; + let bidderRequest; + let result = spec.interpretResponse({body: response}, {bidderRequest}); + + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.have.nested.property('meta.dsa'); + expect(result[0]).to.have.nested.property('ext.dsa'); + expect(result[0].meta.dsa).to.deep.equal(expectedResponse[0].meta.dsa); + expect(result[0].ext.dsa).to.deep.equal(expectedResponse[0].meta.dsa); + }); + }); + describe('native', () => { const adm = { native: { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 317f03752f1..55e8909f6c8 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -21,6 +21,7 @@ import 'modules/priceFloors.js'; import 'modules/multibid/index.js'; import adapterManager from 'src/adapterManager.js'; import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import { deepClone } from '../../../src/utils.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; @@ -33,6 +34,7 @@ describe('the rubicon adapter', function () { logErrorSpy; /** + * @typedef {import('../../../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {Object} sizeMapConverted * @property {string} sizeId * @property {string} size @@ -696,6 +698,16 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); + it('should correctly send cdep signal when requested', () => { + var badposRequest = utils.deepClone(bidderRequest); + badposRequest.bids[0].ortb2 = {device: {ext: {cdep: 3}}}; + + let [request] = spec.buildRequests(badposRequest.bids, badposRequest); + let data = parseQuery(request.data); + + expect(data['o_cdep']).to.equal('3'); + }); + it('ad engine query params should be ordered correctly', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -1229,6 +1241,30 @@ describe('the rubicon adapter', function () { }); }); + it('should still use single request if other rubicon configs are set after', function () { + // set single request to true + config.setConfig({ rubicon: { singleRequest: true } }); + + // execute some other rubicon setConfig + config.setConfig({ rubicon: { netRevenue: true } }); + + const bidCopy = utils.deepClone(bidderRequest.bids[0]); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + + let serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have 1 request only + expect(serverRequests).that.is.an('array').of.length(1); + + // get the built query + let data = parseQuery(serverRequests[0].data); + + // num slots should be 4 + expect(data.slots).to.equal('4'); + }); + it('should not group bid requests if singleRequest does not equal true', function () { config.setConfig({rubicon: {singleRequest: false}}); @@ -1553,6 +1589,22 @@ describe('the rubicon adapter', function () { expect(data['eid_catchall']).to.equal('11111^2'); }); + + it('should send rubiconproject special case', 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: 'rubiconproject.com', + uids: [{ + id: 'some-cool-id', + atype: 3 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_rubiconproject.com']).to.equal('some-cool-id'); + }); }); describe('Config user.id support', function () { @@ -1661,6 +1713,57 @@ describe('the rubicon adapter', function () { expect(data['p_gpid']).to.equal('/1233/sports&div1'); }); + describe('Pass DSA signals', function() { + const ortb2 = { + regs: { + ext: { + dsa: { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [ + { + domain: 'testdomain.com', + dsaparams: [1], + }, + { + domain: 'testdomain2.com', + dsaparams: [1, 2] + } + ] + } + } + } + } + it('should send dsa signals if \"ortb2.regs.ext.dsa\"', function() { + const expectedTransparency = 'testdomain.com~1~~testdomain2.com~1_2' + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2})), bidderRequest) + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('dsarequired'); + expect(data).to.have.property('dsapubrender'); + expect(data).to.have.property('dsadatatopubs'); + expect(data).to.have.property('dsatransparency'); + + expect(data['dsarequired']).to.equal(ortb2.regs.ext.dsa.dsarequired.toString()); + expect(data['dsapubrender']).to.equal(ortb2.regs.ext.dsa.pubrender.toString()); + expect(data['dsadatatopubs']).to.equal(ortb2.regs.ext.dsa.datatopub.toString()); + expect(data['dsatransparency']).to.equal(expectedTransparency) + }) + it('should return one transparency param', function() { + const expectedTransparency = 'testdomain.com~1'; + const ortb2Clone = deepClone(ortb2); + ortb2Clone.regs.ext.dsa.transparency.pop() + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2: ortb2Clone})), bidderRequest) + const data = parseQuery(request.data); + + expect(data).to.be.an('Object'); + expect(data).to.have.property('dsatransparency'); + expect(data['dsatransparency']).to.equal(expectedTransparency); + }) + }) + it('should send gpid and pbadslot since it is prefered over dfp code', function () { bidderRequest.bids[0].ortb2Imp = { ext: { @@ -1768,6 +1871,126 @@ describe('the rubicon adapter', function () { expect(data['tg_i.dfp_ad_unit_code']).to.equal('/a/b/c'); }); }); + + describe('client hints', function () { + let standardSuaObject; + beforeEach(function () { + standardSuaObject = { + source: 2, + platform: { + brand: 'macOS', + version: [ + '12', + '6', + '0' + ] + }, + browsers: [ + { + brand: 'Not.A/Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + brand: 'Chromium', + version: [ + '114', + '0', + '5735', + '198' + ] + }, + { + brand: 'Google Chrome', + version: [ + '114', + '0', + '5735', + '198' + ] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + }); + it('should send m_ch_* params if ortb2.device.sua object is there', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // How should fastlane query be constructed with default SUA + let expectedValues = { + m_ch_arch: 'x86', + m_ch_bitness: '64', + m_ch_ua: `"Not.A/Brand"|v="8","Chromium"|v="114","Google Chrome"|v="114"`, + m_ch_full_ver: `"Not.A/Brand"|v="8.0.0.0","Chromium"|v="114.0.5735.198","Google Chrome"|v="114.0.5735.198"`, + m_ch_mobile: '?0', + m_ch_platform: 'macOS', + m_ch_platform_ver: '12.6.0' + } + + // Build Fastlane call + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // Loop through expected values and if they do not match push an error + const errors = Object.entries(expectedValues).reduce((accum, [key, val]) => { + if (data[key] !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) + return accum; + }, []); + + // should be no errors + expect(errors).to.deep.equal([]); + }); + it('should not send invalid values for m_ch_*', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + + // Alter input SUA object + // send model + standardSuaObject.model = 'Suface Duo'; + // send mobile = 1 + standardSuaObject.mobile = 1; + + // make browsers not an array + standardSuaObject.browsers = 'My Browser'; + + // make platform not have version + delete standardSuaObject.platform.version; + + // delete architecture + delete standardSuaObject.architecture; + + // add SUA to bid + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // Build Fastlane request + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // should show new names + expect(data.m_ch_model).to.equal('Suface Duo'); + expect(data.m_ch_mobile).to.equal('?1'); + + // should still send platform + expect(data.m_ch_platform).to.equal('macOS'); + + // platform version not sent + expect(data).to.not.haveOwnProperty('m_ch_platform_ver'); + + // both ua and full_ver not sent because browsers not array + expect(data).to.not.haveOwnProperty('m_ch_ua'); + expect(data).to.not.haveOwnProperty('m_ch_full_ver'); + + // arch not sent + expect(data).to.not.haveOwnProperty('m_ch_arch'); + }); + }); }); if (FEATURES.VIDEO) { @@ -2045,17 +2268,6 @@ describe('the rubicon adapter', function () { expect(payload.ext.prebid.analytics).to.be.undefined; }); - it('should send video exp param correctly when set', function () { - const bidderRequest = 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 () { const bidderRequest = createVideoBidderRequest(); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -2211,16 +2423,6 @@ describe('the rubicon adapter', function () { bidderRequest = createVideoBidderRequest(); delete bidderRequest.bids[0].mediaTypes.video.linearity; expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // change api to an string, no good - bidderRequest = createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.api = 'string'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // delete api, no good - bidderRequest = createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.api; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); }); it('bid request is valid when video context is outstream', function () { @@ -2549,6 +2751,65 @@ describe('the rubicon adapter', function () { const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); expect(slotParams.kw).to.equal('a,b,c'); }); + + it('should pass along o_ae param when fledge is enabled', () => { + const localBidRequest = Object.assign({}, bidderRequest.bids[0]); + localBidRequest.ortb2Imp.ext.ae = true; + + const slotParams = spec.createSlotParams(localBidRequest, bidderRequest); + + expect(slotParams['o_ae']).to.equal(1) + }); + + it('should pass along desired segtaxes, but not non-desired ones', () => { + const localBidderRequest = Object.assign({}, bidderRequest); + localBidderRequest.refererInfo = {domain: 'bob'}; + config.setConfig({ + rubicon: { + sendUserSegtax: [9], + sendSiteSegtax: [10] + } + }); + localBidderRequest.ortb2.user = { + data: [{ + ext: { + segtax: '404' + }, + segment: [{id: 5}, {id: 6}] + }, { + ext: { + segtax: '508' + }, + segment: [{id: 5}, {id: 2}] + }, { + ext: { + segtax: '9' + }, + segment: [{id: 1}, {id: 2}] + }] + } + localBidderRequest.ortb2.site = { + content: { + data: [{ + ext: { + segtax: '10' + }, + segment: [{id: 2}, {id: 3}] + }, { + ext: { + segtax: '507' + }, + segment: [{id: 3}, {id: 4}] + }] + } + } + const slotParams = spec.createSlotParams(bidderRequest.bids[0], localBidderRequest); + expect(slotParams['tg_i.tax507']).is.equal('3,4'); + expect(slotParams['tg_v.tax508']).is.equal('5,2'); + expect(slotParams['tg_v.tax9']).is.equal('1,2'); + expect(slotParams['tg_i.tax10']).is.equal('2,3'); + expect(slotParams['tg_v.tax404']).is.equal(undefined); + }); }); describe('classifiedAsVideo', function () { @@ -2612,6 +2873,18 @@ describe('the rubicon adapter', function () { expect(request.data.imp).to.have.nested.property('[0].native'); }); + it('should not break if position is set and no video MT', function () { + const bidReq = addNativeToBidRequest(bidderRequest); + delete bidReq.bids[0].mediaTypes.banner; + bidReq.bids[0].params = { + position: 'atf' + } + let [request] = spec.buildRequests(bidReq.bids, bidReq); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(request.data.imp).to.have.nested.property('[0].native'); + }); + describe('that contains also a banner mediaType', function () { it('should send the banner to fastlane BUT NOT the native bid because missing params.video', function() { const bidReq = addNativeToBidRequest(bidderRequest); @@ -2663,6 +2936,35 @@ describe('the rubicon adapter', function () { expect(pbsRequest.data.imp).to.have.nested.property('[0].native'); expect(fastlanteRequest.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); + + it('should include multiformat data in the pbs request', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + expect(pbsRequest.data.imp[0].ext.prebid.bidder.rubicon.formats).to.deep.equal(['native', 'banner']); + }); + + it('should include multiformat data in the fastlane request', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + let formatsIncluded = fastlanteRequest.data.indexOf('formats=native%2Cbanner') !== -1; + expect(formatsIncluded).to.equal(true); + }); }); describe('with bidonmultiformat === false', () => { it('should send only banner request because there\'s no params.video', () => { @@ -2774,7 +3076,7 @@ describe('the rubicon adapter', function () { expect(bids[0].width).to.equal(320); expect(bids[0].height).to.equal(50); expect(bids[0].cpm).to.equal(0.911); - expect(bids[0].ttl).to.equal(300); + expect(bids[0].ttl).to.equal(360); expect(bids[0].netRevenue).to.equal(true); expect(bids[0].rubicon.advertiserId).to.equal(7); expect(bids[0].rubicon.networkId).to.equal(8); @@ -2791,7 +3093,7 @@ describe('the rubicon adapter', function () { expect(bids[1].width).to.equal(300); expect(bids[1].height).to.equal(250); expect(bids[1].cpm).to.equal(0.811); - expect(bids[1].ttl).to.equal(300); + expect(bids[1].ttl).to.equal(360); expect(bids[1].netRevenue).to.equal(true); expect(bids[1].rubicon.advertiserId).to.equal(7); expect(bids[1].rubicon.networkId).to.equal(8); @@ -3066,6 +3368,86 @@ describe('the rubicon adapter', function () { expect(bids[0].cpm).to.be.equal(0); }); + it('should handle DSA object from response', function() { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [ + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '6', + 'adomain': ['test.com'], + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.811, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '15_tier_all_test' + ] + } + ], + 'dsa': { + 'behalf': 'Advertiser', + 'paid': 'Advertiser', + 'transparency': [{ + 'domain': 'dsp1domain.com', + 'dsaparams': [1, 2] + }], + 'adrender': 1 + } + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', + 'size_id': '43', + 'ad_id': '7', + 'adomain': ['test.com'], + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ], + 'dsa': {} + } + ] + }; + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + expect(bids).to.be.lengthOf(2); + expect(bids[1].meta.dsa).to.have.property('behalf'); + expect(bids[1].meta.dsa).to.have.property('paid'); + + // if we dont have dsa field in response or the dsa object is empty + expect(bids[0].meta).to.not.have.property('dsa'); + }) + it('should create bids with matching requestIds if imp id matches', function () { let bidRequests = [{ 'bidder': 'rubicon', @@ -3228,6 +3610,43 @@ describe('the rubicon adapter', function () { expect(bids).to.be.lengthOf(0); }); + it('Should support recieving an auctionConfig and pass it along to Prebid', function () { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [{ + 'status': 'ok', + 'cpm': 0, + 'size_id': 15 + }], + 'component_auction_config': [{ + 'random': 'value', + 'bidId': '5432' + }, + { + 'random': 'string', + 'bidId': '6789' + }] + }; + + let {bids, fledgeAuctionConfigs} = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + + expect(bids).to.be.lengthOf(1); + expect(fledgeAuctionConfigs[0].bidId).to.equal('5432'); + expect(fledgeAuctionConfigs[0].config.random).to.equal('value'); + expect(fledgeAuctionConfigs[1].bidId).to.equal('6789'); + }); + it('should handle an error', function () { let response = { 'status': 'ok', @@ -3486,7 +3905,7 @@ describe('the rubicon adapter', function () { expect(bids[0].seatBidId).to.equal('0'); expect(bids[0].creativeId).to.equal('4259970'); expect(bids[0].cpm).to.equal(2); - expect(bids[0].ttl).to.equal(300); + expect(bids[0].ttl).to.equal(360); 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'); @@ -3520,7 +3939,8 @@ describe('the rubicon adapter', function () { config.setConfig({rubicon: { rendererConfig: { align: 'left', - closeButton: true + closeButton: true, + collapse: false }, rendererUrl: 'https://example.test/renderer.js' }}); @@ -3577,7 +3997,7 @@ describe('the rubicon adapter', function () { expect(bids[0].seatBidId).to.equal('0'); expect(bids[0].creativeId).to.equal('4259970'); expect(bids[0].cpm).to.equal(2); - expect(bids[0].ttl).to.equal(300); + expect(bids[0].ttl).to.equal(360); 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'); @@ -3592,7 +4012,8 @@ describe('the rubicon adapter', function () { expect(typeof bids[0].renderer).to.equal('object'); expect(bids[0].renderer.getConfig()).to.deep.equal({ align: 'left', - closeButton: true + closeButton: true, + collapse: false }); expect(bids[0].renderer.url).to.equal('https://example.test/renderer.js'); }); @@ -3646,7 +4067,7 @@ describe('the rubicon adapter', function () { const renderCall = window.MagniteApex.renderAd.getCall(0); expect(renderCall.args[0]).to.deep.equal({ closeButton: true, - collapse: true, + collapse: false, height: 320, label: undefined, placement: { @@ -3715,7 +4136,7 @@ describe('the rubicon adapter', function () { const renderCall = window.MagniteApex.renderAd.getCall(0); expect(renderCall.args[0]).to.deep.equal({ closeButton: true, - collapse: true, + collapse: false, height: 480, label: undefined, placement: { diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index fb666e89f73..516c5ec933a 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -2,10 +2,24 @@ import { expect } from 'chai'; import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js'; import * as utils from 'src/utils.js'; import { config } from '../../../src/config.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; const PUBLISHER_ID = '0000-0000-01'; const ADUNIT_ID = '000000'; +const adUnitCode = '/19968336/header-bid-tag-0' + +// create a default adunit +const slot = document.createElement('div'); +slot.id = adUnitCode; +slot.style.width = '300px' +slot.style.height = '250px' +slot.style.position = 'absolute' +slot.style.top = '10px' +slot.style.left = '20px' + +document.body.appendChild(slot); + function getSlotConfigs(mediaTypes, params) { return { params: params, @@ -25,7 +39,7 @@ function getSlotConfigs(mediaTypes, params) { tid: 'd704d006-0d6e-4a09-ad6c-179e7e758096', } }, - adUnitCode: 'adunit-code', + adUnitCode: adUnitCode, }; } @@ -46,6 +60,13 @@ const createBannerSlotConfig = (placement, mediatypes) => { }; describe('Seedtag Adapter', function () { + beforeEach(function () { + mockGpt.reset(); + }); + + afterEach(function () { + mockGpt.enable(); + }); describe('isBidRequestValid method', function () { describe('returns true', function () { describe('when banner slot config has all mandatory params', () => { @@ -277,7 +298,7 @@ describe('Seedtag Adapter', function () { expect(data.auctionStart).to.be.greaterThanOrEqual(now); expect(data.ttfb).to.be.greaterThanOrEqual(0); - expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code'); + expect(data.bidRequests[0].adUnitCode).to.equal(adUnitCode); }); describe('GDPR params', function () { @@ -374,6 +395,35 @@ describe('Seedtag Adapter', function () { expect(videoBid.sizes[1][1]).to.equal(600); expect(videoBid.requestCount).to.equal(1); }); + + it('should have geom parameters if slot is available', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + const bidRequests = data.bidRequests; + const bannerBid = bidRequests[0]; + + // on some CI, the DOM is not initialized, so we need to check if the slot is available + const slot = document.getElementById(adUnitCode) + if (slot) { + expect(bannerBid).to.have.property('geom') + + const params = [['width', 300], ['height', 250], ['top', 10], ['left', 20], ['scrollY', 0]] + params.forEach(([param, value]) => { + expect(bannerBid.geom).to.have.property(param) + expect(bannerBid.geom[param]).to.be.a('number') + expect(bannerBid.geom[param]).to.be.equal(value) + }) + + expect(bannerBid.geom).to.have.property('viewport') + const viewportParams = ['width', 'height'] + viewportParams.forEach(param => { + expect(bannerBid.geom.viewport).to.have.property(param) + expect(bannerBid.geom.viewport[param]).to.be.a('number') + }) + } else { + expect(bannerBid).to.not.have.property('geom') + } + }) }); describe('COPPA param', function () { diff --git a/test/spec/modules/setupadBidAdapter_spec.js b/test/spec/modules/setupadBidAdapter_spec.js new file mode 100644 index 00000000000..d4ff73d005f --- /dev/null +++ b/test/spec/modules/setupadBidAdapter_spec.js @@ -0,0 +1,348 @@ +import { spec } from 'modules/setupadBidAdapter.js'; + +describe('SetupadAdapter', function () { + const userIdAsEids = [ + { + source: 'pubcid.org', + uids: [ + { + atype: 1, + id: '01EAJWWNEPN3CYMM5N8M5VXY22', + }, + ], + }, + ]; + + const bidRequests = [ + { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'rubicon', + bidderRequestId: '15246a574e859f', + uspConsent: 'usp-context-string', + gdprConsent: { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + gdprApplies: true, + }, + params: { + placement_id: '123', + account_id: 'test-account-id', + }, + sizes: [[300, 250]], + ortb2: { + device: { + w: 1500, + h: 1000, + }, + site: { + domain: 'test.com', + page: 'http://test.com', + }, + }, + userIdAsEids, + }, + ]; + + const bidderRequest = { + ortb2: { + device: { + w: 1500, + h: 1000, + }, + }, + refererInfo: { + domain: 'test.com', + page: 'http://test.com', + ref: '', + }, + }; + + const serverResponse = { + body: { + id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', + seatbid: [ + { + bid: [ + { + id: 'test-bid-id', + price: 0.8, + adm: 'this is an ad', + adid: 'test-ad-id', + adomain: ['test.addomain.com'], + w: 300, + h: 250, + }, + ], + seat: 'testBidder', + }, + ], + cur: 'USD', + ext: { + sync: { + image: ['urlA?gdpr={{.GDPR}}'], + iframe: ['urlB'], + }, + }, + }, + }; + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'setupad', + params: { + placement_id: '123', + }, + }; + 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 () { + delete bid.params.placement_id; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('check request params with GDPR and USP', function () { + const request = spec.buildRequests(bidRequests, bidRequests[0]); + expect(JSON.parse(request[0].data).user.ext.consent).to.equal( + 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA' + ); + expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); + expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('usp-context-string'); + }); + + it('check request params without GDPR', function () { + let bidRequestsWithoutGDPR = Object.assign({}, bidRequests[0]); + delete bidRequestsWithoutGDPR.gdprConsent; + const request = spec.buildRequests([bidRequestsWithoutGDPR], bidRequestsWithoutGDPR); + expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; + expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('usp-context-string'); + }); + + it('should return correct storedrequest id if account_id is provided', function () { + const request = spec.buildRequests(bidRequests, bidRequests[0]); + expect(JSON.parse(request[0].data).ext.prebid.storedrequest.id).to.equal('test-account-id'); + }); + + it('should return correct storedrequest id if account_id is not provided', function () { + let bidRequestsWithoutAccountId = Object.assign({}, bidRequests[0]); + delete bidRequestsWithoutAccountId.params.account_id; + const request = spec.buildRequests( + [bidRequestsWithoutAccountId], + bidRequestsWithoutAccountId + ); + expect(JSON.parse(request[0].data).ext.prebid.storedrequest.id).to.equal('default'); + }); + + it('validate generated params', function () { + const request = spec.buildRequests(bidRequests); + expect(request[0].bidId).to.equal('22c4871113f461'); + expect(JSON.parse(request[0].data).id).to.equal('15246a574e859f'); + }); + + it('check if correct site object was added', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const siteObj = JSON.parse(request[0].data).site; + + expect(siteObj.domain).to.equal('test.com'); + expect(siteObj.page).to.equal('http://test.com'); + expect(siteObj.ref).to.equal(''); + }); + + it('check if correct device object was added', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const deviceObj = JSON.parse(request[0].data).device; + + expect(deviceObj.w).to.equal(1500); + expect(deviceObj.h).to.equal(1000); + }); + + it('check if imp object was added', function () { + const request = spec.buildRequests(bidRequests); + expect(JSON.parse(request[0].data).imp).to.be.an('array'); + }); + + it('should send "user.ext.eids" in the request for Prebid.js supported modules only', function () { + const request = spec.buildRequests(bidRequests); + expect(JSON.parse(request[0].data).user.ext.eids).to.deep.equal(userIdAsEids); + }); + + it('should send an undefined "user.ext.eids" in the request if userId module is unsupported', function () { + let bidRequestsUnsupportedUserIdModule = Object.assign({}, bidRequests[0]); + delete bidRequestsUnsupportedUserIdModule.userIdAsEids; + const request = spec.buildRequests(bidRequestsUnsupportedUserIdModule); + + expect(JSON.parse(request[0].data).user.ext.eids).to.be.undefined; + }); + }); + + describe('getUserSyncs', () => { + it('should return user sync', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true, + }; + const responses = [ + { + body: { + ext: { + responsetimemillis: { + 'test seat 1': 2, + 'test seat 2': 1, + }, + }, + }, + }, + ]; + const gdprConsent = { + gdprApplies: 1, + consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', + }; + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'; + const expectedUserSyncs = [ + { + type: 'iframe', + url: 'https://cookie.stpd.cloud/sync?bidders=%5B%22test%20seat%201%22%2C%22test%20seat%202%22%5D&gdpr=1&gdpr_consent=dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig&usp_consent=mkjvbiniwot4827obfoy8sdg8203gb&type=iframe', + }, + ]; + + const userSyncs = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent); + + expect(userSyncs).to.deep.equal(expectedUserSyncs); + }); + + it('should return empty user syncs when responsetimemillis is not defined', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true, + }; + const responses = [ + { + body: { + ext: {}, + }, + }, + ]; + const gdprConsent = { + gdprApplies: 1, + consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', + }; + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'; + const expectedUserSyncs = []; + + const userSyncs = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent); + + expect(userSyncs).to.deep.equal(expectedUserSyncs); + }); + }); + + describe('interpretResponse', function () { + it('should return empty array if error during parsing', () => { + const wrongServerResponse = 'wrong data'; + let request = spec.buildRequests(bidRequests, bidRequests[0]); + let result = spec.interpretResponse(wrongServerResponse, request); + + expect(result).to.be.instanceof(Array); + expect(result.length).to.equal(0); + }); + + it('should get correct bid response', function () { + const result = spec.interpretResponse(serverResponse, bidRequests[0]); + expect(result).to.be.an('array').with.lengthOf(1); + expect(result[0].requestId).to.equal('22c4871113f461'); + expect(result[0].cpm).to.equal(0.8); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal('test-bid-id'); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(360); + expect(result[0].ad).to.equal('this is an ad'); + }); + }); + + describe('onBidWon', function () { + it('should stop if bidder is not equal to BIDDER_CODE', function () { + const bid = { + bidder: 'rubicon', + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is not provided', function () { + const bid = { + bidder: 'setupad', + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is empty array', function () { + const bid = { + bidder: 'setupad', + params: [], + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is not array', function () { + expect( + spec.onBidWon({ + bidder: 'setupad', + params: {}, + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: 'test', + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: 1, + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: null, + }) + ).to.be.undefined; + + expect( + spec.onBidWon({ + bidder: 'setupad', + params: undefined, + }) + ).to.be.undefined; + }); + + it('should stop if bid.params.placement_id is not provided', function () { + const bid = { + bidder: 'setupad', + params: [{ account_id: 'test' }], + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + + it('should stop if bid.params is not provided and bid.bids is not an array', function () { + const bid = { + bidder: 'setupad', + params: undefined, + bids: {}, + }; + const result = spec.onBidWon(bid); + expect(result).to.be.undefined; + }); + }); +}); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 4989dcb1098..ab099d87429 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; +import * as sinon from 'sinon'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config'; import * as utils from 'src/utils'; @@ -73,7 +73,10 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', bidId: 'bidId1', transactionId: 'transactionId1', - sizes: [[300, 250], [300, 600]], + sizes: [ + [300, 250], + [300, 600], + ], params: { pkey: 'aaaa1111', bcat: ['cat1', 'cat2'], @@ -95,104 +98,104 @@ describe('sharethrough adapter spec', function () { }, userIdAsEids: [ { - 'source': 'pubcid.org', - 'uids': [ + source: 'pubcid.org', + uids: [ { - 'atype': 1, - 'id': 'fake-pubcid' + atype: 1, + id: 'fake-pubcid', }, - ] + ], }, { - 'source': 'liveramp.com', - 'uids': [ + source: 'liveramp.com', + uids: [ { - 'atype': 1, - 'id': 'fake-identity-link' - } - ] + atype: 1, + id: 'fake-identity-link', + }, + ], }, { - 'source': 'id5-sync.com', - 'uids': [ + source: 'id5-sync.com', + uids: [ { - 'atype': 1, - 'id': 'fake-id5id' - } - ] + atype: 1, + id: 'fake-id5id', + }, + ], }, { - 'source': 'adserver.org', - 'uids': [ + source: 'adserver.org', + uids: [ { - 'atype': 1, - 'id': 'fake-tdid' - } - ] + atype: 1, + id: 'fake-tdid', + }, + ], }, { - 'source': 'criteo.com', - 'uids': [ + source: 'criteo.com', + uids: [ { - 'atype': 1, - 'id': 'fake-criteo' - } - ] + atype: 1, + id: 'fake-criteo', + }, + ], }, { - 'source': 'britepool.com', - 'uids': [ + source: 'britepool.com', + uids: [ { - 'atype': 1, - 'id': 'fake-britepool' - } - ] + atype: 1, + id: 'fake-britepool', + }, + ], }, { - 'source': 'liveintent.com', - 'uids': [ + source: 'liveintent.com', + uids: [ { - 'atype': 1, - 'id': 'fake-lipbid' - } - ] + atype: 1, + id: 'fake-lipbid', + }, + ], }, { - 'source': 'intentiq.com', - 'uids': [ + source: 'intentiq.com', + uids: [ { - 'atype': 1, - 'id': 'fake-intentiq' - } - ] + atype: 1, + id: 'fake-intentiq', + }, + ], }, { - 'source': 'crwdcntrl.net', - 'uids': [ + source: 'crwdcntrl.net', + uids: [ { - 'atype': 1, - 'id': 'fake-lotame' - } - ] + atype: 1, + id: 'fake-lotame', + }, + ], }, { - 'source': 'parrable.com', - 'uids': [ + source: 'parrable.com', + uids: [ { - 'atype': 1, - 'id': 'fake-parrable' - } - ] + atype: 1, + id: 'fake-parrable', + }, + ], }, { - 'source': 'netid.de', - 'uids': [ + source: 'netid.de', + uids: [ { - 'atype': 1, - 'id': 'fake-netid' - } - ] - } + atype: 1, + id: 'fake-netid', + }, + ], + }, ], crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj', @@ -250,10 +253,10 @@ describe('sharethrough adapter spec', function () { }, ortb2: { source: { - tid: 'auction-id' - } + tid: 'auction-id', + }, }, - timeout: 242 + timeout: 242, }; }); @@ -312,6 +315,12 @@ describe('sharethrough adapter spec', function () { expect(eid.uids[0].atype).to.be.ok; } + // expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + // expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); + + // expect(openRtbReq.regs.ext.gpp).to.equal(bidderRequest.ortb2.regs.gpp); + // expect(openRtbReq.regs.ext.gpp_sid).to.equal(bidderRequest.ortb2.regs.gpp_sid); + expect(openRtbReq.device.ua).to.equal(navigator.userAgent); expect(openRtbReq.regs.coppa).to.equal(1); @@ -395,6 +404,60 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.regs.coppa).to.equal(0); }); }); + + describe('gpp', () => { + it('should properly attach GPP information to the request when applicable', () => { + bidderRequest.gppConsent = { + gppString: 'some-gpp-string', + applicableSections: [3, 5], + }; + + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; + expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); + }); + + it('should populate request accordingly when gpp explicitly does not apply', function () { + const openRtbReq = spec.buildRequests(bidRequests, {})[0].data; + + expect(openRtbReq.regs.gpp).to.be.undefined; + }); + }); + }); + + describe('dsa', () => { + it('should properly attach dsa information to the request when applicable', () => { + bidderRequest.ortb2 = { + regs: { + ext: { + dsa: { + 'dsarequired': 1, + 'pubrender': 0, + 'datatopub': 1, + 'transparency': [{ + 'domain': 'good-domain', + 'dsaparams': [1, 2] + }, { + 'domain': 'bad-setup', + 'dsaparams': ['1', 3] + }] + } + } + } + } + + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; + expect(openRtbReq.regs.ext.dsa.dsarequired).to.equal(1); + expect(openRtbReq.regs.ext.dsa.pubrender).to.equal(0); + expect(openRtbReq.regs.ext.dsa.datatopub).to.equal(1); + expect(openRtbReq.regs.ext.dsa.transparency).to.deep.equal([{ + 'domain': 'good-domain', + 'dsaparams': [1, 2] + }, { + 'domain': 'bad-setup', + 'dsaparams': ['1', 3] + }]); + }); }); describe('transaction id at the impression level', () => { @@ -455,7 +518,10 @@ describe('sharethrough adapter spec', function () { const bannerImp = builtRequest.data.imp[0].banner; expect(bannerImp.pos).to.equal(1); expect(bannerImp.topframe).to.equal(1); - expect(bannerImp.format).to.deep.equal([{ w: 300, h: 250 }, { w: 300, h: 600 }]); + expect(bannerImp.format).to.deep.equal([ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + ]); }); it('should default to pos 0 if not provided', () => { @@ -554,6 +620,64 @@ describe('sharethrough adapter spec', function () { expect(videoImp.placement).to.equal(4); }); + + it('should not override "placement" value if "plcmt" prop is present', () => { + // ASSEMBLE + const ARBITRARY_PLACEMENT_VALUE = 99; + const ARBITRARY_PLCMT_VALUE = 100; + + bidRequests[1].mediaTypes.video.context = 'instream'; + bidRequests[1].mediaTypes.video.placement = ARBITRARY_PLACEMENT_VALUE; + + // adding "plcmt" property - this should prevent "placement" prop + // from getting overridden to 1 + bidRequests[1].mediaTypes.video['plcmt'] = ARBITRARY_PLCMT_VALUE; + + // ACT + const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; + const videoImp = builtRequest.data.imp[0].video; + + // ASSERT + expect(videoImp.placement).to.equal(ARBITRARY_PLACEMENT_VALUE); + expect(videoImp.plcmt).to.equal(ARBITRARY_PLCMT_VALUE); + }); + }); + }); + + describe('cookie deprecation', () => { + it('should not add cdep if we do not get it in an impression request', () => { + const builtRequests = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: { + propThatIsNotCdep: 'value-we-dont-care-about', + }, + }, + }, + }); + const noCdep = builtRequests.every((builtRequest) => { + const ourCdepValue = builtRequest.data.device?.ext?.cdep; + return ourCdepValue === undefined; + }); + expect(noCdep).to.be.true; + }); + + it('should add cdep if we DO get it in an impression request', () => { + const builtRequests = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: { + cdep: 'cdep-value', + }, + }, + }, + }); + const cdepPresent = builtRequests.every((builtRequest) => { + return builtRequest.data.device.ext.cdep === 'cdep-value'; + }); + expect(cdepPresent).to.be.true; }); }); @@ -585,10 +709,14 @@ describe('sharethrough adapter spec', function () { }, bcat: ['IAB1', 'IAB2-1'], badv: ['domain1.com', 'domain2.com'], + regs: { + gpp: 'gpp_string', + gpp_sid: [7], + }, }; it('should include first party data in open rtb request, site section', () => { - const openRtbReq = spec.buildRequests(bidRequests, {...bidderRequest, ortb2: firstPartyData})[0].data; + const openRtbReq = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: firstPartyData })[0].data; expect(openRtbReq.site.name).to.equal(firstPartyData.site.name); expect(openRtbReq.site.keywords).to.equal(firstPartyData.site.keywords); @@ -612,6 +740,29 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.bcat).to.deep.equal(firstPartyData.bcat); expect(openRtbReq.badv).to.deep.equal(firstPartyData.badv); }); + + it('should include first party data in open rtb request, regulation section', () => { + const openRtbReq = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: firstPartyData })[0].data; + + expect(openRtbReq.regs.ext.gpp).to.equal(firstPartyData.regs.gpp); + expect(openRtbReq.regs.ext.gpp_sid).to.equal(firstPartyData.regs.gpp_sid); + }); + }); + + describe('fledge', () => { + it('should attach "ae" as a property to the request if 1) fledge auctions are enabled, and 2) request is display (only supporting display for now)', () => { + // ASSEMBLE + const EXPECTED_AE_VALUE = 1; + + // ACT + bidderRequest['fledgeEnabled'] = true; + const builtRequests = spec.buildRequests(bidRequests, bidderRequest); + const ACTUAL_AE_VALUE = builtRequests[0].data.imp[0].ext.ae; + + // ASSERT + expect(ACTUAL_AE_VALUE).to.equal(EXPECTED_AE_VALUE); + expect(builtRequests[1].data.imp[0].ext.ae).to.be.undefined; + }); }); }); @@ -624,26 +775,31 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { - seatbid: [{ - bid: [{ - id: '123', - impid: 'bidId1', - w: 300, - h: 250, - price: 42, - crid: 'creative', - dealid: 'deal', - adomain: ['domain.com'], - adm: 'markup', - }, { - id: '456', - impid: 'bidId2', - w: 640, - h: 480, - price: 42, - adm: 'vastTag', - }], - }], + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, + { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }, + ], + }, + ], }, }; }); @@ -673,16 +829,20 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[1]; response = { body: { - seatbid: [{ - bid: [{ - id: '456', - impid: 'bidId2', - w: 640, - h: 480, - price: 42, - adm: 'vastTag', - }], - }], + seatbid: [ + { + bid: [ + { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }, + ], + }, + ], }, }; }); @@ -712,24 +872,28 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { - seatbid: [{ - bid: [{ - id: '123', - impid: 'bidId1', - w: 300, - h: 250, - price: 42, - crid: 'creative', - dealid: 'deal', - adomain: ['domain.com'], - adm: 'markup', - }], - }], + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, + ], + }, + ], }, }; }); - it('should have null optional fields when the response\'s optional seatbid[].bid[].ext field is empty', () => { + it("should have null optional fields when the response's optional seatbid[].bid[].ext field is empty", () => { const bid = spec.interpretResponse(response, request)[0]; expect(bid.meta.networkId).to.be.null; @@ -747,7 +911,7 @@ describe('sharethrough adapter spec', function () { expect(bid.meta.mediaType).to.be.null; }); - it('should have populated fields when the response\'s optional seatbid[].bid[].ext fields are filled', () => { + it("should have populated fields when the response's optional seatbid[].bid[].ext fields are filled", () => { response.body.seatbid[0].bid[0].ext = { networkId: 'my network id', networkName: 'my network name', @@ -792,8 +956,8 @@ describe('sharethrough adapter spec', function () { expect(syncArray).to.deep.equal([ { type: 'image', url: 'cookieUrl1' }, { type: 'image', url: 'cookieUrl2' }, - { type: 'image', url: 'cookieUrl3' }], - ); + { type: 'image', url: 'cookieUrl3' }, + ]); }); it('returns an empty array if serverResponses is empty', function () { diff --git a/test/spec/modules/shinezRtbBidAdapter_spec.js b/test/spec/modules/shinezRtbBidAdapter_spec.js new file mode 100644 index 00000000000..3965cd69c5f --- /dev/null +++ b/test/spec/modules/shinezRtbBidAdapter_spec.js @@ -0,0 +1,639 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/shinezRtbBidAdapter'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; +import {deepAccess} from 'src/utils.js'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId', 'digitrustid']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppConsent': { + 'gppString': 'gpp_string', + 'applicableSections': [7] + }, + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['sweetgum.io'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('ShinezRtbBidAdapter', function () { + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + shinezRtb: { + storageAllowed: true + } + }; + sandbox = sinon.sandbox.create(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + prebidVersion: version, + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + }, + gpid: '0123456789' + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.sweetgum.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }) + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['sweetgum.io'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['sweetgum.io'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['sweetgum.io'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'digitrustid': + return {data: {id}}; + case 'lipb': + return {lipbid: id}; + case 'parrableId': + return {eid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + shinezRtb: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + shinezRtb: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem('myKey', 2020); + const {value, created} = getStorageItem('myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem('myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); diff --git a/test/spec/modules/silvermobBidAdapter_spec.js b/test/spec/modules/silvermobBidAdapter_spec.js new file mode 100644 index 00000000000..7d7fbacc04e --- /dev/null +++ b/test/spec/modules/silvermobBidAdapter_spec.js @@ -0,0 +1,301 @@ +import { expect } from 'chai'; +import {spec} from '../../../modules/silvermobBidAdapter.js'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from '../../../src/config.js'; +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; + +// load modules that register ORTB processors +import 'src/prebid.js'; +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; + +const SIMPLE_BID_REQUEST = { + bidder: 'silvermob', + params: { + zoneid: '0', + host: 'us', + }, + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1499748733608-0', + transactionId: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + bidId: '33e9500b21129f', + bidderRequestId: '2772c1e566670b', + auctionId: '192721e36a0239', + sizes: [[300, 250], [160, 600]], + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', + }, +} + +const BANNER_BID_REQUEST = { + bidder: 'silvermob', + params: { + zoneid: '0', + host: 'us', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + code: 'banner_example', + timeout: 1000, +} + +const VIDEO_BID_REQUEST = { + placementCode: '/DfpAccount1/slotVideo', + bidId: 'test-bid-id-2', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + bidder: 'silvermob', + params: { + zoneid: '0', + host: 'us', + }, + adUnitCode: '/adunit-code/test-path', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, +} + +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: 'silvermob', + params: { + zoneid: '0', + host: 'us', + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, + uspConsent: 'uspConsent' +}; + +const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } +}; + +const gdprConsent = { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', +} + +describe('silvermobAdapter', 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('with user privacy regulations', function () { + it('should send the Coppa "required" flag set to "1" in the request', function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(serverRequest.data.regs.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should send the GDPR Consent data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({ ...bidderRequest, gdprConsent })); + expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); + }); + + it('should send the CCPA data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(true); + }); + + it('should return false when zoneid is missing', function () { + let localbid = Object.assign({}, BANNER_BID_REQUEST); + delete localbid.params.zoneid; + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(false); + }); + }); + + describe('build request', function () { + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], syncAddFPDToBidderRequest(bidderRequest)); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request).to.not.equal('array'); + expect(request.data).to.be.an('object'); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://us.silvermob.com/marketplace/api/dsp/prebidjs/0'); + + expect(request.data.site).to.have.property('page'); + expect(request.data.site).to.have.property('domain'); + expect(request.data).to.have.property('id'); + expect(request.data).to.have.property('imp'); + expect(request.data).to.have.property('device'); + }); + + it('should return a valid bid BANNER request object', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); + expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); + }); + + if (FEATURES.VIDEO) { + it('should return a valid bid VIDEO request object', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.w).to.be.an('number'); + expect(request.data.imp[0].video.h).to.be.an('number'); + }); + } + + it('should return a valid bid NATIVE request object', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0]).to.be.an('object'); + }); + }) + + describe('interpretResponse', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [{ + 'bidId': '28ffdk2B952532', + 'bidder': 'silvermob', + 'userId': { + 'freepassId': { + 'userIp': '172.21.0.1', + 'userId': '123', + 'commonId': 'commonIdValue' + } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' + } + }]; + bidderRequest = {}; + }); + + it('Empty response must return empty array', function () { + const emptyResponse = null; + let response = spec.interpretResponse(emptyResponse, BANNER_BID_REQUEST); + + expect(response).to.be.an('array').that.is.empty; + }) + + it('Should interpret banner response', function () { + const serverResponse = { + body: { + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'impid': '28ffdk2B952532', + 'price': 97, + 'adm': '', + 'w': 300, + 'h': 250, + 'crid': 'creative0' + }] + }] + } + }; + it('should interpret server response', function () { + const bidRequest = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(serverResponse, bidRequest); + expect(bids).to.be.an('array'); + const bid = bids[0]; + expect(bid).to.be.an('object'); + expect(bid.currency).to.equal('USD'); + expect(bid.cpm).to.equal(97); + expect(bid.ad).to.equal(ad) + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('creative0'); + }); + }) + }); +}); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 9daa6a87826..b01d95e2a4c 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -786,8 +786,8 @@ describe('Smart bid adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); - expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(null); - expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); + expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); + expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(1); }); it('Verify videoData params override meta values', function () { @@ -833,6 +833,73 @@ describe('Smart bid adapter tests', function () { expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(6); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(3); }); + + it('should pass additional parameters', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + api: [1, 2, 3], + maxbitrate: 50, + minbitrate: 20, + maxduration: 30, + minduration: 5, + placement: 3, + playbackmethod: [2, 4], + playerSize: [[640, 480]], + plcmt: 1, + skip: 0 + } + }, + params: { + siteId: '123' + } + }]); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).to.have.property('iabframeworks').and.to.equal('1,2,3'); + expect(requestContent.videoData).not.to.have.property('skip'); + expect(requestContent.videoData).to.have.property('vbrmax').and.to.equal(50); + expect(requestContent.videoData).to.have.property('vbrmin').and.to.equal(20); + expect(requestContent.videoData).to.have.property('vdmax').and.to.equal(30); + expect(requestContent.videoData).to.have.property('vdmin').and.to.equal(5); + expect(requestContent.videoData).to.have.property('vplcmt').and.to.equal(1); + expect(requestContent.videoData).to.have.property('vpmt').and.to.have.lengthOf(2); + expect(requestContent.videoData.vpmt[0]).to.equal(2); + expect(requestContent.videoData.vpmt[1]).to.equal(4); + expect(requestContent.videoData).to.have.property('vpt').and.to.equal(3); + }); + + it('should not pass not valuable parameters', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + maxbitrate: 20, + minbitrate: null, + maxduration: 0, + playbackmethod: [], + playerSize: [[640, 480]], + plcmt: 1 + } + }, + params: { + siteId: '123' + } + }]); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).not.to.have.property('iabframeworks'); + expect(requestContent.videoData).to.have.property('vbrmax').and.to.equal(20); + expect(requestContent.videoData).not.to.have.property('vbrmin'); + expect(requestContent.videoData).not.to.have.property('vdmax'); + expect(requestContent.videoData).not.to.have.property('vdmin'); + expect(requestContent.videoData).to.have.property('vplcmt').and.to.equal(1); + expect(requestContent.videoData).not.to.have.property('vpmt'); + expect(requestContent.videoData).not.to.have.property('vpt'); + }); }); }); @@ -1029,8 +1096,8 @@ describe('Smart bid adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); - expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(null); - expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); + expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); + expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(1); }); it('Verify videoData params override meta values', function () { @@ -1076,6 +1143,50 @@ describe('Smart bid adapter tests', function () { expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(6); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(3); }); + + it('should handle value of videoMediaType.startdelay', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + startdelay: -2 + } + }, + params: { + siteId: 123, + pageId: 456, + formatId: 78 + } + }]); + + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('videoData'); + expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(3); + }); + + it('should return specified value of videoMediaType.startdelay', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]], + startdelay: 60 + } + }, + params: { + siteId: 123, + pageId: 456, + formatId: 78 + } + }]); + + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('videoData'); + expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); + }); }); describe('External ids tests', function () { @@ -1393,4 +1504,41 @@ describe('Smart bid adapter tests', function () { expect(requestContent).to.have.property('gpid').and.to.equal(gpid); }); }); + + describe('#getValuableProperty method', function () { + it('should return an object when calling with a number value', () => { + const obj = spec.getValuableProperty('prop', 3); + expect(obj).to.deep.equal({ prop: 3 }); + }); + + it('should return an empty object when calling with a string value', () => { + const obj = spec.getValuableProperty('prop', 'str'); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a number property', () => { + const obj = spec.getValuableProperty(7, 'str'); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a null value', () => { + const obj = spec.getValuableProperty('prop', null); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with an object value', () => { + const obj = spec.getValuableProperty('prop', {}); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a 0 value', () => { + const obj = spec.getValuableProperty('prop', 0); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling without the value argument', () => { + const obj = spec.getValuableProperty('prop'); + expect(obj).to.deep.equal({}); + }); + }); }); diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index cbc4a6405e8..1b592e142c3 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/smartyadsBidAdapter.js'; import { config } from '../../../src/config.js'; +import {server} from '../../mocks/xhr'; describe('SmartyadsAdapter', function () { let bid = { @@ -14,6 +15,21 @@ describe('SmartyadsAdapter', function () { } }; + let bidResponse = { + width: 300, + height: 250, + mediaType: 'banner', + ad: `test mode`, + requestId: '23fhj33i987f', + cpm: 0.1, + ttl: 120, + creativeId: '123', + netRevenue: true, + currency: 'USD', + dealId: 'HASH', + sid: 1234 + }; + describe('isBidRequestValid', function () { it('Should return true if there are bidId, params and sourceid parameters present', function () { expect(spec.isBidRequestValid(bid)).to.be.true; @@ -36,12 +52,16 @@ describe('SmartyadsAdapter', function () { expect(serverRequest.method).to.equal('POST'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'); + expect(serverRequest.url).to.be.oneOf([ + 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' + ]); }); 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', 'coppa'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa', 'eeid', 'ifa'); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); expect(data.coppa).to.be.a('number'); @@ -257,4 +277,79 @@ describe('SmartyadsAdapter', function () { ]); }); }); + + describe('onBidWon', function () { + it('should exists', function () { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid won notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'winTest': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidWon(bid); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onTimeout', function () { + it('should exists', function () { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid timeout notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidTimeout': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onTimeout(bid); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onBidderError', function () { + it('should exists', function () { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bidder error notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidderError': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidderError(bid); + expect(server.requests.length).to.equal(1); + }); + }); }); diff --git a/test/spec/modules/smilewantedBidAdapter_spec.js b/test/spec/modules/smilewantedBidAdapter_spec.js index 22221dbe1ef..99c4034610f 100644 --- a/test/spec/modules/smilewantedBidAdapter_spec.js +++ b/test/spec/modules/smilewantedBidAdapter_spec.js @@ -93,7 +93,24 @@ const BID_RESPONSE_DISPLAY = { const VIDEO_INSTREAM_REQUEST = [{ code: 'video1', mediaTypes: { - video: {} + video: { + context: 'instream', + mimes: ['video/mp4'], + minduration: 0, + maxduration: 120, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + startdelay: 0, + placement: 1, + skip: 1, + skipafter: 10, + minbitrate: 10, + maxbitrate: 10, + delivery: [1], + playbackmethod: [2], + api: [1, 2], + linearity: 1, + playerSize: [640, 480] + } }, sizes: [ [640, 480] @@ -163,6 +180,99 @@ const BID_RESPONSE_VIDEO_OUTSTREAM = { } }; +const NATIVE_REQUEST = [{ + adUnitCode: 'native_300x250', + code: '/19968336/prebid_native_example_1', + bidId: '12345', + sizes: [ + [300, 250] + ], + mediaTypes: { + native: { + sendTargetingKeys: false, + title: { + required: true, + len: 140 + }, + image: { + required: true, + sizes: [300, 250] + }, + icon: { + required: false, + sizes: [50, 50] + }, + sponsoredBy: { + required: true + }, + body: { + required: true + }, + clickUrl: { + required: false + }, + privacyLink: { + required: false + }, + cta: { + required: false + }, + rating: { + required: false + }, + likes: { + required: false + }, + downloads: { + required: false + }, + price: { + required: false + }, + salePrice: { + required: false + }, + phone: { + required: false + }, + address: { + required: false + }, + desc2: { + required: false + }, + displayUrl: { + required: false + } + } + }, + bidder: 'smilewanted', + params: { + zoneId: 4, + }, + requestId: 'request_abcd1234', + ortb2Imp: { + ext: { + tid: 'trans_abcd1234', + } + }, +}]; + +const BID_RESPONSE_NATIVE = { + body: { + cpm: 3, + width: 300, + height: 250, + creativeId: 'crea_sw_1', + currency: 'EUR', + isNetCpm: true, + ttl: 300, + ad: '{"link":{"url":"https://www.smilewanted.com"},"assets":[{"id":0,"required":1,"title":{"len":50}},{"id":1,"required":1,"img":{"type":3,"w":150,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":2,"required":0,"img":{"type":1,"w":50,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":3,"required":1,"data":{"type":1,"value":"Smilewanted sponsor"}},{"id":4,"required":1,"data":{"type":2,"value":"Smilewanted Description"}}]}', + cSyncUrl: 'https://csync.smilewanted.com', + formatTypeSw: 'native' + } +}; + // Default params with optional ones describe('smilewantedBidAdapterTests', function () { it('SmileWanted - Verify build request', function () { @@ -195,6 +305,23 @@ describe('smilewantedBidAdapterTests', function () { expect(requestVideoInstreamContent.sizes[0]).to.have.property('w').and.to.equal(640); expect(requestVideoInstreamContent.sizes[0]).to.have.property('h').and.to.equal(480); expect(requestVideoInstreamContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestVideoInstreamContent).to.have.property('videoParams'); + expect(requestVideoInstreamContent.videoParams).to.have.property('context').and.to.equal('instream').and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('mimes').to.be.an('array').that.include('video/mp4').and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('minduration').and.to.equal(0).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('maxduration').and.to.equal(120).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('protocols').to.be.an('array').that.include.members([1, 2, 3, 4, 5, 6, 7, 8]).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('startdelay').and.to.equal(0).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('placement').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('skip').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('skipafter').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('minbitrate').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('maxbitrate').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('delivery').to.be.an('array').that.include(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('playbackmethod').to.be.an('array').that.include(2).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('api').to.be.an('array').that.include.members([1, 2]).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('linearity').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('playerSize').to.be.an('array').that.include.members([640, 480]).and.to.not.be.undefined; const requestVideoOutstream = spec.buildRequests(VIDEO_OUTSTREAM_REQUEST); expect(requestVideoOutstream[0]).to.have.property('url').and.to.equal('https://prebid.smilewanted.com'); @@ -206,6 +333,39 @@ describe('smilewantedBidAdapterTests', function () { expect(requestVideoOutstreamContent.sizes[0]).to.have.property('w').and.to.equal(640); expect(requestVideoOutstreamContent.sizes[0]).to.have.property('h').and.to.equal(480); expect(requestVideoOutstreamContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + + const requestNative = spec.buildRequests(NATIVE_REQUEST); + expect(requestNative[0]).to.have.property('url').and.to.equal('https://prebid.smilewanted.com'); + expect(requestNative[0]).to.have.property('method').and.to.equal('POST'); + const requestNativeContent = JSON.parse(requestNative[0].data); + expect(requestNativeContent).to.have.property('zoneId').and.to.equal(4); + expect(requestNativeContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestNativeContent).to.have.property('sizes'); + expect(requestNativeContent.sizes[0]).to.have.property('w').and.to.equal(300); + expect(requestNativeContent.sizes[0]).to.have.property('h').and.to.equal(250); + expect(requestNativeContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestNativeContent).to.have.property('context').and.to.equal('native').and.to.not.be.undefined; + expect(requestNativeContent).to.have.property('nativeParams'); + expect(requestNativeContent.nativeParams.title).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.title).to.have.property('len').and.to.equal(140); + expect(requestNativeContent.nativeParams.image).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.image).to.have.property('sizes').to.be.an('array').that.include.members([300, 250]).and.to.not.be.undefined; + expect(requestNativeContent.nativeParams.icon).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.icon).to.have.property('sizes').to.be.an('array').that.include.members([50, 50]).and.to.not.be.undefined; + expect(requestNativeContent.nativeParams.sponsoredBy).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.body).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.clickUrl).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.privacyLink).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.cta).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.rating).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.likes).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.downloads).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.price).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.salePrice).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.phone).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.address).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.desc2).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.displayUrl).to.have.property('required').and.to.equal(false); }); it('SmileWanted - Verify build request with referrer', function () { @@ -337,7 +497,7 @@ describe('smilewantedBidAdapterTests', function () { }).to.not.throw(); }); - it('SmileWanted - Verify parse response - Video Oustream', function () { + it('SmileWanted - Verify parse response - Video Outstream', function () { const request = spec.buildRequests(VIDEO_OUTSTREAM_REQUEST); const bids = spec.interpretResponse(BID_RESPONSE_VIDEO_OUTSTREAM, request[0]); expect(bids).to.have.lengthOf(1); @@ -360,6 +520,28 @@ describe('smilewantedBidAdapterTests', function () { }).to.not.throw(); }); + it('SmileWanted - Verify parse response - Native', function () { + const request = spec.buildRequests(NATIVE_REQUEST); + const bids = spec.interpretResponse(BID_RESPONSE_NATIVE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(3); + expect(bid.ad).to.equal('{"link":{"url":"https://www.smilewanted.com"},"assets":[{"id":0,"required":1,"title":{"len":50}},{"id":1,"required":1,"img":{"type":3,"w":150,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":2,"required":0,"img":{"type":1,"w":50,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":3,"required":1,"data":{"type":1,"value":"Smilewanted sponsor"}},{"id":4,"required":1,"data":{"type":2,"value":"Smilewanted Description"}}]}'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crea_sw_1'); + expect(bid.currency).to.equal('EUR'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(NATIVE_REQUEST[0].bidId); + + expect(function () { + spec.interpretResponse(BID_RESPONSE_NATIVE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + it('SmileWanted - Verify bidder code', function () { expect(spec.code).to.equal('smilewanted'); }); diff --git a/test/spec/modules/snigelBidAdapter_spec.js b/test/spec/modules/snigelBidAdapter_spec.js index 3fc09493f03..828aec9491c 100644 --- a/test/spec/modules/snigelBidAdapter_spec.js +++ b/test/spec/modules/snigelBidAdapter_spec.js @@ -2,6 +2,8 @@ import {expect} from 'chai'; import {spec} from 'modules/snigelBidAdapter.js'; import {config} from 'src/config.js'; import {isValid} from 'src/adapters/bidderFactory.js'; +import {registerActivityControl} from 'src/activities/rules.js'; +import {ACTIVITY_ACCESS_DEVICE} from 'src/activities/activities.js'; const BASE_BID_REQUEST = { adUnitCode: 'top_leaderboard', @@ -20,8 +22,10 @@ const makeBidRequest = function (overrides) { }; const BASE_BIDDER_REQUEST = { + auctionId: 'test', bidderRequestId: 'test', refererInfo: { + page: 'https://localhost', canonicalUrl: 'https://localhost', }, }; @@ -53,8 +57,8 @@ describe('snigelBidAdapter', function () { it('should build a single request for every impression and its placement', function () { const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); const bidRequests = [ - makeBidRequest({bidId: 'a', params: {placement: 'top_leaderboard'}}), - makeBidRequest({bidId: 'b', params: {placement: 'bottom_leaderboard'}}), + makeBidRequest({bidId: 'a', adUnitCode: 'au_a', params: {placement: 'top_leaderboard'}}), + makeBidRequest({bidId: 'b', adUnitCode: 'au_b', params: {placement: 'bottom_leaderboard'}}), ]; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -70,9 +74,9 @@ describe('snigelBidAdapter', function () { expect(data).to.have.property('page').and.to.equal('https://localhost'); expect(data).to.have.property('placements'); expect(data.placements.length).to.equal(2); - expect(data.placements[0].uuid).to.equal('a'); + expect(data.placements[0].id).to.equal('au_a'); expect(data.placements[0].name).to.equal('top_leaderboard'); - expect(data.placements[1].uuid).to.equal('b'); + expect(data.placements[1].id).to.equal('au_b'); expect(data.placements[1].name).to.equal('bottom_leaderboard'); }); @@ -127,6 +131,56 @@ describe('snigelBidAdapter', function () { const data = JSON.parse(request.data); expect(data).to.have.property('coppa').and.to.equal(true); }); + + it('should forward refresh information', function () { + const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); + const topLeaderboard = makeBidRequest({adUnitCode: 'top_leaderboard'}); + const bottomLeaderboard = makeBidRequest({adUnitCode: 'bottom_leaderboard'}); + const sidebar = makeBidRequest({adUnitCode: 'sidebar'}); + + // first auction, no refresh + let request = spec.buildRequests([topLeaderboard, bottomLeaderboard], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.be.undefined; + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.be.undefined; + + // second auction for top leaderboard, was refreshed + request = spec.buildRequests([topLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(1); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('sidebar'); + expect(data.placements[1].refresh).to.be.undefined; + + // third auction, all units refreshed at some point + request = spec.buildRequests([topLeaderboard, bottomLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(3); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(2); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.not.be.undefined; + expect(data.placements[1].refresh.count).to.equal(1); + expect(data.placements[1].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[2].id).to.equal('sidebar'); + expect(data.placements[2].refresh).to.not.be.undefined; + expect(data.placements[2].refresh.count).to.equal(1); + expect(data.placements[2].refresh.time).to.be.greaterThanOrEqual(0); + }); }); describe('interpretResponse', function () { @@ -146,7 +200,7 @@ describe('snigelBidAdapter', function () { cur: 'USD', bids: [ { - uuid: BASE_BID_REQUEST.bidId, + id: BASE_BID_REQUEST.adUnitCode, price: 0.0575, ad: '

Test Ad

', width: 728, @@ -160,7 +214,7 @@ describe('snigelBidAdapter', function () { }, }; - const bids = spec.interpretResponse(serverResponse, {}); + const bids = spec.interpretResponse(serverResponse, {bidderRequest: {bids: [BASE_BID_REQUEST]}}); expect(bids.length).to.equal(1); const bid = bids[0]; expect(isValid(BASE_BID_REQUEST.adUnitCode, bid)).to.be.true; @@ -292,5 +346,67 @@ describe('snigelBidAdapter', function () { expect(sync).to.have.property('url'); expect(sync.url).to.equal(`https://somesyncurl?gdpr=1&gdpr_consent=${DUMMY_GDPR_CONSENT_STRING}`); }); + + it('should omit session ID if no device access', function() { + const bidderRequest = makeBidderRequest(); + const unregisterRule = registerActivityControl(ACTIVITY_ACCESS_DEVICE, 'denyAccess', () => { + return {allow: false, reason: 'no consent'}; + }); + + try { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + const data = JSON.parse(request.data); + expect(data.sessionId).to.be.undefined; + } finally { + unregisterRule(); + } + }); + + it('should determine full GDPR consent correctly', function () { + const baseBidderRequest = makeBidderRequest({ + gdprConsent: { + gdprApplies: true, + vendorData: { + purpose: { + consents: {1: true, 2: true, 3: true, 4: true, 5: true}, + }, + vendor: { + consents: {[spec.gvlid]: true}, + } + }, + } + }); + let request = spec.buildRequests([], baseBidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + expect(data.gdprConsent).to.be.true; + + let bidderRequest = {...baseBidderRequest, ...{gdprConsent: {vendorData: {purpose: {consents: {1: false}}}}}}; + request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data.gdprConsent).to.be.false; + + bidderRequest = {...baseBidderRequest, ...{gdprConsent: {vendorData: {vendor: {consents: {[spec.gvlid]: false}}}}}}; + request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data.gdprConsent).to.be.false; + }); + + it('should increment auction counter upon every request', function() { + const bidderRequest = makeBidderRequest({}); + + let request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + const previousCounter = data.counter; + + request = spec.buildRequests([], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data.counter).to.equal(previousCounter + 1); + }); }); }); diff --git a/test/spec/modules/sonobiAnalyticsAdapter_spec.js b/test/spec/modules/sonobiAnalyticsAdapter_spec.js index 76ff88836d4..ed8ccd22eea 100644 --- a/test/spec/modules/sonobiAnalyticsAdapter_spec.js +++ b/test/spec/modules/sonobiAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import sonobiAnalytics from 'modules/sonobiAnalyticsAdapter.js'; +import sonobiAnalytics, {DEFAULT_EVENT_URL} from 'modules/sonobiAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; let events = require('src/events'); @@ -76,8 +76,8 @@ describe('Sonobi Prebid Analytic', function () { events.emit(constants.EVENTS.AUCTION_END, {auctionId: '13', bidsReceived: [bid]}); clock.tick(5000); - expect(server.requests).to.have.length(1); - expect(JSON.parse(server.requests[0].requestBody)).to.have.length(3) + const req = server.requests.find(req => req.url.indexOf(DEFAULT_EVENT_URL) !== -1); + expect(JSON.parse(req.requestBody)).to.have.length(3) done(); }); }); diff --git a/test/spec/modules/sonobiBidAdapter_spec.js b/test/spec/modules/sonobiBidAdapter_spec.js index de8d0a5bda7..c7f954cfdcf 100644 --- a/test/spec/modules/sonobiBidAdapter_spec.js +++ b/test/spec/modules/sonobiBidAdapter_spec.js @@ -1,9 +1,9 @@ -import { expect } from 'chai' -import { spec, _getPlatform } from 'modules/sonobiBidAdapter.js' -import { newBidder } from 'src/adapters/bidderFactory.js' +import { expect } from 'chai'; +import { _getPlatform, spec } from 'modules/sonobiBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import { userSync } from '../../../src/userSync.js'; import { config } from 'src/config.js'; -import * as utils from '../../../src/utils.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; describe('SonobiBidAdapter', function () { const adapter = newBidder(spec) @@ -248,13 +248,13 @@ describe('SonobiBidAdapter', function () { let sandbox; beforeEach(function () { sinon.stub(userSync, 'canBidderRegisterSync'); - sinon.stub(utils, 'getGptSlotInfoForAdUnitCode') + sinon.stub(gptUtils, 'getGptSlotInfoForAdUnitCode') .onFirstCall().returns({ gptSlot: '/123123/gpt_publisher/adunit-code-3', divId: 'adunit-code-3-div-id' }); sandbox = sinon.createSandbox(); }); afterEach(function () { userSync.canBidderRegisterSync.restore(); - utils.getGptSlotInfoForAdUnitCode.restore(); + gptUtils.getGptSlotInfoForAdUnitCode.restore(); sandbox.restore(); }); let bidRequest = [{ @@ -295,7 +295,10 @@ describe('SonobiBidAdapter', function () { mediaTypes: { video: { playerSize: [640, 480], - context: 'outstream' + context: 'outstream', + playbackmethod: [1, 2, 3], + plcmt: 3, + placement: 2 } } }, @@ -339,7 +342,7 @@ describe('SonobiBidAdapter', function () { }]; let keyMakerData = { - '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,', + '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,pm=1:2:3,p=2,pl=3,', '30b31c1838de1d': '1a2b3c4d5e6f1a2b3c4e|300x250,300x600|f=0.42,gpid=/123123/gpt_publisher/adunit-code-3,c=d,', '/7780971/sparks_prebid_LB|30b31c1838de1e': '300x250,300x600|gpid=/7780971/sparks_prebid_LB,c=d,', }; @@ -356,7 +359,9 @@ describe('SonobiBidAdapter', function () { 'page': 'https://example.com', 'stack': ['https://example.com'] }, - uspConsent: 'someCCPAString' + uspConsent: 'someCCPAString', + ortb2: {} + }; it('should set fpd if there is any data in ortb2', function () { @@ -490,6 +495,12 @@ describe('SonobiBidAdapter', function () { expect(bidRequests.data.hfa).to.equal('hfakey') }) + it('should return a properly formatted request with experianRtidData and exexperianRtidKeypKey omitted from fpd', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.data.fpd.indexOf('experianRtidData')).to.equal(-1); + expect(bidRequests.data.fpd.indexOf('exexperianRtidKeypKey')).to.equal(-1); + }); + it('should return null if there is nothing to bid on', function () { const bidRequests = spec.buildRequests([{ params: {} }], bidderRequests) expect(bidRequests).to.equal(null); diff --git a/test/spec/modules/sovrnAnalyticsAdapter_spec.js b/test/spec/modules/sovrnAnalyticsAdapter_spec.js index 68552eb3d8a..d0363eab144 100644 --- a/test/spec/modules/sovrnAnalyticsAdapter_spec.js +++ b/test/spec/modules/sovrnAnalyticsAdapter_spec.js @@ -12,8 +12,8 @@ let constants = require('src/constants.json'); /** * Emit analytics events - * @param {array} eventArr - array of objects to define the events that will fire - * @param {object} eventObj - key is eventType, value is event + * @param {Array} eventType - array of objects to define the events that will fire + * @param {object} event - key is eventType, value is event * @param {string} auctionId - the auction id to attached to the events */ function emitEvent(eventType, event, auctionId) { diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 90913c6f130..274192d14a7 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -64,6 +64,29 @@ describe('sovrnBidAdapter', function() { expect(spec.isBidRequestValid(bidRequest)).to.equal(false) }) + + it('should return true when minduration is not passed', function() { + const width = 300 + const height = 250 + const mimes = ['video/mp4', 'application/javascript'] + const protocols = [2, 5] + const maxduration = 60 + const startdelay = 0 + const videoBidRequest = { + ...baseBidRequest, + mediaTypes: { + video: { + mimes, + protocols, + playerSize: [[width, height], [360, 240]], + maxduration, + startdelay + } + } + } + + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true) + }) }) describe('buildRequests', function () { @@ -295,6 +318,41 @@ describe('sovrnBidAdapter', function() { expect(data.regs.ext['us_privacy']).to.equal(bidderRequest.uspConsent) }) + it('should not set coppa when coppa is undefined', function () { + const bidderRequest = { + ...baseBidderRequest, + bidderCode: 'sovrn', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + bids: [baseBidRequest], + gdprConsent: { + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + gdprApplies: true + }, + } + const {regs} = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(regs.coppa).to.be.undefined + }) + + it('should set coppa to 1 when coppa is provided with value true', function () { + const bidderRequest = { + ...baseBidderRequest, + ortb2: { + regs: { + coppa: true + } + }, + bidderCode: 'sovrn', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + bids: [baseBidRequest] + } + const {regs} = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(regs.coppa).to.equal(1) + }) + it('should send gpp info in OpenRTB 2.6 location when gppConsent defined', function () { const bidderRequest = { ...baseBidderRequest, @@ -472,6 +530,45 @@ describe('sovrnBidAdapter', function() { expect(impression.bidfloor).to.equal(2.00) }) + it('floor should be undefined if there is no floor from the floor module and params', function() { + const floorBid = { + ...baseBidRequest + } + floorBid.params = { + tagid: 1234 + } + const request = spec.buildRequests([floorBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.bidfloor).to.be.undefined + }) + it('floor should be undefined if there is incorrect floor value from the floor module', function() { + const floorBid = { + ...baseBidRequest, + getFloor: () => ({currency: 'USD', floor: 'incorrect_value'}), + params: { + tagid: 1234 + } + } + const request = spec.buildRequests([floorBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.bidfloor).to.be.undefined + }) + it('floor should be undefined if there is incorrect floor value from the params', function() { + const floorBid = { + ...baseBidRequest, + getFloor: () => ({}) + } + floorBid.params = { + tagid: 1234, + bidfloor: 'incorrect_value' + } + const request = spec.buildRequests([floorBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.bidfloor).to.be.undefined + }) describe('First Party Data', function () { it('should provide first party data if provided', function() { const ortb2 = { diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js new file mode 100644 index 00000000000..293f7da30a1 --- /dev/null +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -0,0 +1,467 @@ +import {expect} from 'chai'; +import { deepClone, mergeDeep } from 'src/utils'; +import {spec as adapter} from 'modules/sparteoBidAdapter'; + +const CURRENCY = 'EUR'; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; + +const VALID_BID_BANNER = { + bidder: 'sparteo', + bidId: '1a2b3c4d', + adUnitCode: 'id-1234', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + formats: ['corner'] + }, + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + } +}; + +const VALID_BID_VIDEO = { + bidder: 'sparteo', + bidId: '5e6f7g8h', + adUnitCode: 'id-5678', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + }, + mediaTypes: { + video: { + playerSize: [640, 360], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + api: [1, 2], + mimes: ['video/mp4'], + skip: 1, + startdelay: 0, + placement: 1, + linearity: 1, + minduration: 5, + maxduration: 30, + context: 'instream' + } + }, + ortb2Imp: { + ext: { + pbadslot: 'video' + } + } +}; + +const VALID_REQUEST_BANNER = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + }, + 'ext': { + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'formats': ['corner'] + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST_VIDEO = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video', + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + }, + 'ext': { + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'formats': ['corner'] + } + } + } + }, { + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video', + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const BIDDER_REQUEST = { + bids: [VALID_BID_BANNER, VALID_BID_VIDEO] +} + +const BIDDER_REQUEST_BANNER = { + bids: [VALID_BID_BANNER] +} + +const BIDDER_REQUEST_VIDEO = { + bids: [VALID_BID_VIDEO] +} + +describe('SparteoAdapter', function () { + describe('isBidRequestValid', function () { + describe('Check method return', function () { + it('should return true', function () { + expect(adapter.isBidRequestValid(VALID_BID_BANNER)).to.equal(true); + expect(adapter.isBidRequestValid(VALID_BID_VIDEO)).to.equal(true); + }); + + it('should return false because the networkId is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + delete wrongBid.params.networkId; + + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the banner size is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + + wrongBid.mediaTypes.banner.sizes = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.banner.sizes; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the video player size paramater is missing', function () { + let wrongBid = deepClone(VALID_BID_VIDEO); + + wrongBid.mediaTypes.video.playerSize = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.video.playerSize; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('Check method return', function () { + if (FEATURES.VIDEO) { + it('should return the right formatted requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST); + }); + } + + it('should return the right formatted banner requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_BANNER); + }); + + if (FEATURES.VIDEO) { + it('should return the right formatted video requests', function() { + const request = adapter.buildRequests([VALID_BID_VIDEO], BIDDER_REQUEST_VIDEO); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_VIDEO); + }); + } + + it('should return the right formatted request with endpoint test', function() { + let endpoint = 'https://bid-test.sparteo.com/auction'; + + let bids = mergeDeep(deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]), { + params: { + endpoint: endpoint + } + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST)); + + const request = adapter.buildRequests(bids, BIDDER_REQUEST); + requests.url = endpoint; + delete request.data.id; + + expect(requests).to.deep.equal(requests); + }); + }); + }); + + describe('interpretResponse', function() { + describe('Check method return', function () { + it('should return the right formatted response', function() { + let response = { + body: { + 'id': '63f4d300-6896-4bdc-8561-0932f73148b1', + 'cur': 'EUR', + 'seatbid': [ + { + 'seat': 'sparteo', + 'group': 0, + 'bid': [ + { + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87a', + 'impid': '1a2b3c4d', + 'price': 4.5, + 'ext': { + 'prebid': { + 'type': 'banner' + } + }, + 'adm': 'script', + 'crid': 'crid', + 'w': 1, + 'h': 1, + 'nurl': 'https://t.bidder.sparteo.com/img' + } + ] + } + ] + } + }; + + if (FEATURES.VIDEO) { + response.body.seatbid[0].bid.push({ + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87b', + 'impid': '5e6f7g8h', + 'price': 5, + 'ext': { + 'prebid': { + 'type': 'video', + 'cache': { + 'vastXml': { + 'url': 'https://pbs.tet.com/cache?uuid=1234' + } + } + } + }, + 'adm': 'tag', + 'crid': 'crid', + 'w': 640, + 'h': 480, + 'nurl': 'https://t.bidder.sparteo.com/img' + }); + } + + let formattedReponse = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
' + } + ]; + + if (FEATURES.VIDEO) { + formattedReponse.push({ + requestId: '5e6f7g8h', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + cpm: 5, + width: 640, + height: 480, + playerWidth: 640, + playerHeight: 360, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + nurl: 'https://t.bidder.sparteo.com/img', + vastUrl: 'https://pbs.tet.com/cache?uuid=1234', + vastXml: 'tag' + }); + } + + if (FEATURES.VIDEO) { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } else { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } + }); + }); + }); + + describe('onBidWon', function() { + describe('Check methods succeed', function () { + it('should not throw error', function() { + let bids = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
', + nurl: [ + 'win.domain.com' + ] + }, + { + requestId: '2570', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + id: 'id-5678', + cpm: 5, + width: 640, + height: 480, + creativeId: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastXml: 'vast xml', + nurl: [ + 'win.domain2.com' + ] + } + ]; + + bids.forEach(function(bid) { + expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); + }); + }); + }); + }); + + describe('getUserSyncs', function() { + describe('Check methods succeed', function () { + it('should return the sync url', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + }; + const gdprConsent = { + gdprApplies: 1, + consentString: 'tcfv2' + }; + const uspConsent = { + consentString: '1Y---' + }; + + const syncUrls = [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + '&gdpr=1&gdpr_consent=tcfv2&usp_consent=1Y---' + }]; + + expect(adapter.getUserSyncs(syncOptions, null, gdprConsent, uspConsent)).to.deep.equal(syncUrls); + }); + }); + }); +}); diff --git a/test/spec/modules/ssmasBidAdapter_spec.js b/test/spec/modules/ssmasBidAdapter_spec.js new file mode 100644 index 00000000000..26c6f60da4b --- /dev/null +++ b/test/spec/modules/ssmasBidAdapter_spec.js @@ -0,0 +1,244 @@ +import { expect } from 'chai'; +import { spec, SSMAS_CODE, SSMAS_ENDPOINT, SSMAS_REQUEST_METHOD } from 'modules/ssmasBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; + +describe('ssmasBidAdapter', function () { + const bid = { + bidder: SSMAS_CODE, + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + params: { + placementId: '1' + } + }; + + const bidderRequest = { + 'bidderCode': SSMAS_CODE, + 'auctionId': 'd912faa2-174f-4636-b755-7396a0a964d8', + 'bidderRequestId': '109db5a5f5c6788', + 'bids': [ + bid + ], + 'auctionStart': 1684799653734, + 'timeout': 20000, + 'metrics': {}, + 'ortb2': { + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/noadserver/basic_noadserver.html', + 'ref': 'http://localhost:9999/integrationExamples/noadserver/' + }, + 'device': { + 'w': 1536, + 'h': 711, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'language': 'es' + } + }, + 'start': 1684799653737 + }; + + describe('Build Requests', () => { + it('Check bid request', function () { + const request = spec.buildRequests([bid], bidderRequest); + expect(request[0].method).to.equal(SSMAS_REQUEST_METHOD); + expect(request[0].url).to.equal(SSMAS_ENDPOINT); + }); + }); + + describe('register adapter functions', () => { + const adapter = newBidder(spec); + it('is registered', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('validate bid request building', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('test bad bid request', function () { + // empty bid + expect(spec.isBidRequestValid({bidId: '', params: {}})).to.be.false; + + // empty bidId + bid.bidId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + + // empty placementId + bid.bidId = '1231'; + bid.params.placementId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('check bid request bidder is Sem Seo & Mas', function() { + const invalidBid = { + ...bid, bidder: 'invalidBidder' + }; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('interpretResponse', function () { + let bidOrtbResponse = { + 'id': 'aa02e2fe-56d9-4713-88f9-d8672ceae8ab', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0001', + 'impid': '3919400af0b73e8', + 'price': 7.01, + 'adid': null, + 'nurl': null, + 'adm': '', + 'adomain': [ + 'ssmas.com' + ], + 'iurl': null, + 'cid': null, + 'crid': '3547894', + 'attr': [], + 'api': 0, + 'protocol': 0, + 'dealid': null, + 'h': 600, + 'w': 300, + 'cat': null, + 'ext': null, + 'builder': { + 'id': '0001', + 'adid': null, + 'impid': '3919400af0b73e8', + 'adomainList': [ + 'ssmas.com' + ], + 'attrList': [] + }, + 'adomainList': [ + 'ssmas.com' + ], + 'attrList': [] + } + ], + 'seat': null, + 'group': 0 + } + ], + 'bidid': '408731cc-c018-4976-bfc6-89f9c61e97a0', + 'cur': 'EUR', + 'nbr': -1 + }; + let bidResponse = { + 'mediaType': 'banner', + 'ad': '', + 'requestId': '37c658fe8ba57b', + 'seatBidId': '0001', + 'cpm': 10, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'dealId': null, + 'creative_id': '3547894', + 'creativeId': '3547894', + 'ttl': 30, + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'ssmas.com' + ] + } + }; + let bidRequest = { + 'imp': [ + { + 'ext': { + 'tid': '937db9c3-c22d-4454-b786-fcad76a349e5', + 'data': { + 'pbadslot': 'test-div' + } + }, + 'id': '3919400af0b73e8', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + }, + { + 'ext': { + 'tid': '0c0d3d1b-0ad0-4786-896d-24c15fc6531d', + 'data': { + 'pbadslot': 'test-div2' + } + }, + 'id': '3919400af0b73e8', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + } + ], + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/noadserver/basic_noadserver.html', + 'ref': 'http://localhost:9999/integrationExamples/noadserver/', + 'id': 1, + 'ext': { + 'placementId': 13144370 + } + }, + 'device': { + 'w': 1536, + 'h': 711, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'language': 'es' + }, + 'id': '8cc2f4b0-084d-4f40-acfa-5bec2023b1ab', + 'test': 0, + 'tmax': 20000, + 'source': { + 'tid': '8cc2f4b0-084d-4f40-acfa-5bec2023b1ab' + } + } + }); + + describe('test onBidWon function', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index a95f08314b5..2f5fe104eb1 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -38,7 +38,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }, { @@ -60,7 +60,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '2', + bidId: bidderRequestId + '2', transactionId, } ]; @@ -83,7 +83,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bid_native = { @@ -122,7 +122,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bid_video = { @@ -144,7 +144,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bids_timeouted = [{ @@ -155,7 +155,7 @@ describe('SSPBC adapter', function () { siteId: '8816', }], auctionId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', timeout: 100, }, { @@ -166,7 +166,7 @@ describe('SSPBC adapter', function () { siteId: '8816', }], auctionId, - bidId: auctionId + '2', + bidId: bidderRequestId + '2', timeout: 100, } ]; @@ -198,7 +198,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }]; const bidRequest = { @@ -293,7 +293,7 @@ describe('SSPBC adapter', function () { }; const serverResponse = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', @@ -333,7 +333,7 @@ describe('SSPBC adapter', function () { }; const serverResponseSingle = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', @@ -358,11 +358,11 @@ describe('SSPBC adapter', function () { }; const serverResponseOneCode = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -385,11 +385,11 @@ describe('SSPBC adapter', function () { }; const serverResponseVideo = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -413,11 +413,11 @@ describe('SSPBC adapter', function () { }; const serverResponseNative = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -438,7 +438,7 @@ describe('SSPBC adapter', function () { }; const emptyResponse = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, } } return { @@ -638,7 +638,7 @@ describe('SSPBC adapter', function () { expect(adcode).to.be.a('string'); expect(adcode).to.contain('window.rekid'); expect(adcode).to.contain('window.mcad'); - expect(adcode).to.contain('window.gdpr'); + expect(adcode).to.contain('window.tcString'); expect(adcode).to.contain('window.page'); expect(adcode).to.contain('window.requestPVID'); }); @@ -696,7 +696,7 @@ describe('SSPBC adapter', function () { 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('requestId').that.equals(bid.bidderRequestId); expect(notificationPayload).to.have.property('tagid').that.deep.equals([bid.adUnitCode]); expect(notificationPayload).to.have.property('siteId').that.is.an('array'); expect(notificationPayload).to.have.property('slotId').that.is.an('array'); @@ -717,7 +717,6 @@ describe('SSPBC adapter', function () { 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('tagid').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); }); }); diff --git a/test/spec/modules/stnBidAdapter_spec.js b/test/spec/modules/stnBidAdapter_spec.js new file mode 100644 index 00000000000..deba87baac2 --- /dev/null +++ b/test/spec/modules/stnBidAdapter_spec.js @@ -0,0 +1,625 @@ +import { expect } from 'chai'; +import { spec } from 'modules/stnBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT = 'https://hb.stngo.com/hb-multi'; +const TEST_ENDPOINT = 'https://hb.stngo.com/hb-multi-test'; +const TTL = 360; +/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ + +describe('stnAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream', + 'plcmt': 1 + } + }, + 'vastXml': '"..."' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'loop': 1, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'loop': 2, + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'stn', + } + const placementId = '12345678'; + const api = [1, 2]; + const mimes = ['application/javascript', 'video/mp4', 'video/quicktime']; + const protocols = [2, 3, 5, 6]; + + it('sends the placementId to ENDPOINT via POST', function () { + bidRequests[0].params.placementId = placementId; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].placementId).to.equal(placementId); + }); + + it('sends the plcmt to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].plcmt).to.equal(1); + }); + + it('sends the is_wrapper parameter to ENDPOINT via POST', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('is_wrapper'); + expect(request.data.params.is_wrapper).to.equal(false); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to TEST ENDPOINT via POST', function () { + const request = spec.buildRequests(testModeBidRequests, bidderRequest); + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send the correct bid Id', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); + }); + + it('should send the correct supported api array', function () { + bidRequests[0].mediaTypes.video.api = api; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].api).to.be.an('array'); + expect(request.data.bids[0].api).to.eql([1, 2]); + }); + + it('should send the correct mimes array', function () { + bidRequests[1].mediaTypes.banner.mimes = mimes; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[1].mimes).to.be.an('array'); + expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + }); + + it('should send the correct protocols array', function () { + bidRequests[0].mediaTypes.video.protocols = protocols; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].protocols).to.be.an('array'); + expect(request.data.bids[0].protocols).to.eql([2, 3, 5, 6]); + }); + + it('should send the correct sizes array', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sizes).to.be.an('array'); + expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) + expect(request.data.bids[1].sizes).to.be.an('array'); + expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + }); + + it('should send the correct media type', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].mediaType).to.equal(VIDEO) + expect(request.data.bids[1].mediaType).to.equal(BANNER) + }); + + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); + config.setConfig({ + userSync: { + syncEnabled: true, + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); + }); + + it('should not send the gpp param if gppConsent is false in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: false}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gpp'); + expect(request.data.params).to.not.have.property('gpp_sid'); + }); + + it('should send the gpp param if gppConsent is true in the bidRequest', function () { + const bidderRequestWithGPP = Object.assign({gppConsent: {gppString: 'test-consent-string', applicableSections: [7]}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGPP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gpp', 'test-consent-string'); + expect(request.data.params.gpp_sid[0]).to.be.equal(7); + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + }); + + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); + }); + + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); + }); + + it('should check sua param in bid request', function() { + const sua = { + 'platform': { + 'brand': 'macOS', + 'version': ['12', '4', '0'] + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Not;A=Brand', + 'version': [ '99', '0', '0', '0' ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'device': { + 'sua': { + 'platform': { + 'brand': 'macOS', + 'version': [ '12', '4', '0' ] + }, + 'browsers': [ + { + 'brand': 'Chromium', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Google Chrome', + 'version': [ '106', '0', '5249', '119' ] + }, + { + 'brand': 'Not;A=Brand', + 'version': [ '99', '0', '0', '0' ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + } + } + const requestWithSua = spec.buildRequests([bid], bidderRequest); + const data = requestWithSua.data; + expect(data.bids[0].sua).to.exist; + expect(data.bids[0].sua).to.deep.equal(sua); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sua).to.not.exist; + }); + + describe('COPPA Param', function() { + it('should set coppa equal 0 in bid request if coppa is set to false', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(0); + }); + + it('should set coppa equal 1 in bid request if coppa is set to true', function() { + const bid = utils.deepClone(bidRequests[0]); + bid.ortb2 = { + 'regs': { + 'coppa': true, + } + }; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].coppa).to.be.equal(1); + }); + }); + }); + + describe('interpretResponse', function () { + const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [{ + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: VIDEO + }, + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER + }] + }; + + const expectedVideoResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 640, + height: 480, + ttl: TTL, + creativeId: '21e12606d47ba7', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: VIDEO, + meta: { + mediaType: VIDEO, + advertiserDomains: ['abc.com'] + }, + vastXml: '', + }; + + const expectedBannerResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, + currency: 'USD', + width: 640, + height: 480, + ttl: TTL, + creativeId: '21e12606d47ba7', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: BANNER, + meta: { + mediaType: BANNER, + advertiserDomains: ['abc.com'] + }, + ad: '""' + }; + + it('should get correct bid response', function () { + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); + expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + }); + + it('video type should have vastXml key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[0].vastXml).to.equal(expectedVideoResponse.vastXml) + }); + + it('banner type should have ad key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[1].ad).to.equal(expectedBannerResponse.ad) + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + } + }; + + const iframeSyncResponse = { + body: { + params: { + userSyncURL: 'https://iframe-sync-url.test' + } + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) +}); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 2ed5f80f152..66e0da6ddf8 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -844,6 +844,39 @@ describe('stroeerCore bid adapter', function () { assert.nestedPropertyVal(bid, 'ban.fp.cur', 'EUR'); assert.deepNestedPropertyVal(bid, 'ban.fp.siz', [{w: 160, h: 60, p: 2.7}]); }); + + it('should add the DSA signals', () => { + const bidReq = buildBidderRequest(); + const dsa = { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [ + { + domain: 'testplatform.com', + dsaparams: [1], + }, + { + domain: 'testdomain.com', + dsaparams: [1, 2] + } + ] + } + const ortb2 = { + regs: { + ext: { + dsa + } + } + } + + bidReq.ortb2 = utils.deepClone(ortb2); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + const sentOrtb2 = serverRequestInfo.data.ortb2; + + assert.deepEqual(sentOrtb2, ortb2); + }); }); }); }); @@ -882,13 +915,32 @@ describe('stroeerCore bid adapter', function () { assertStandardFieldsOnVideoBid(videoBidResponse, 'bid1', 'video', 800, 250, 4); }) - it('should add data to meta object', () => { + it('should add advertiser domains to meta object', () => { const response = buildBidderResponse(); response.bids[0] = Object.assign(response.bids[0], {adomain: ['website.org', 'domain.com']}); const result = spec.interpretResponse({body: response}); - assert.deepPropertyVal(result[0], 'meta', {advertiserDomains: ['website.org', 'domain.com']}); - // nothing provided for the second bid - assert.deepPropertyVal(result[1], 'meta', {advertiserDomains: undefined}); + assert.deepPropertyVal(result[0].meta, 'advertiserDomains', ['website.org', 'domain.com']); + assert.propertyVal(result[1].meta, 'advertiserDomains', undefined); + }); + + it('should add dsa info to meta object', () => { + const dsaResponse = { + behalf: 'AdvertiserA', + paid: 'AdvertiserB', + transparency: [{ + domain: 'dspexample.com', + dsaparams: [1, 2], + }], + adrender: 1 + }; + + const response = buildBidderResponse(); + response.bids[0] = Object.assign(response.bids[0], {dsa: utils.deepClone(dsaResponse)}); + + const result = spec.interpretResponse({body: response}); + + assert.deepPropertyVal(result[0].meta, 'dsa', dsaResponse); + assert.propertyVal(result[1].meta, 'dsa', undefined); }); }); diff --git a/test/spec/modules/stvBidAdapter_spec.js b/test/spec/modules/stvBidAdapter_spec.js index 41f29cced34..3ef865ed2f1 100644 --- a/test/spec/modules/stvBidAdapter_spec.js +++ b/test/spec/modules/stvBidAdapter_spec.js @@ -71,6 +71,24 @@ describe('stvAdapter', function() { 'hp': 1 } ] + }, + 'userId': { + 'id5id': { + 'uid': '1234', + 'ext': { + 'linkType': 'abc' + } + }, + 'netId': '2345', + 'uid2': { + 'id': '3456', + }, + 'sharedid': { + 'id': '4567', + }, + 'idl_env': '5678', + 'criteoId': '6789', + 'utiq': '7890', } }, { @@ -84,7 +102,27 @@ describe('stvAdapter', function() { ], 'bidId': '30b31c1838de1e2', 'bidderRequestId': '22edbae2733bf62', - 'auctionId': '1d1a030790a476' + 'auctionId': '1d1a030790a476', + 'userId': { // with other utiq variant + 'id5id': { + 'uid': '1234', + 'ext': { + 'linkType': 'abc' + } + }, + 'netId': '2345', + 'uid2': { + 'id': '3456', + }, + 'sharedid': { + 'id': '4567', + }, + 'idl_env': '5678', + 'criteoId': '6789', + 'utiq': { + 'id': '7890' + }, + } }, { 'bidder': 'stv', 'params': { @@ -181,7 +219,7 @@ describe('stvAdapter', function() { expect(request1.method).to.equal('GET'); expect(request1.url).to.equal(ENDPOINT_URL); let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&schain=1.0,0!reseller.com,aaaaa,1,BidRequest4,,,&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&bcat=IAB2%2CIAB4&dvt=desktop&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&schain=1.0,0!reseller.com,aaaaa,1,BidRequest4,,&uids=id5%3A1234,id5_linktype%3Aabc,netid%3A2345,uid2%3A3456,sharedid%3A4567,liverampid%3A5678,criteoid%3A6789,utiq%3A7890&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&bcat=IAB2%2CIAB4&dvt=desktop&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); }); var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; @@ -189,7 +227,7 @@ describe('stvAdapter', function() { expect(request2.method).to.equal('GET'); expect(request2.url).to.equal(ENDPOINT_URL); let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=html&alternative=prebid_js&_ps=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&prebidDevMode=1&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&uids=id5%3A1234,id5_linktype%3Aabc,netid%3A2345,uid2%3A3456,sharedid%3A4567,liverampid%3A5678,criteoid%3A6789,utiq%3A7890&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&prebidDevMode=1&media_types%5Bbanner%5D=300x250'); }); // Without gdprConsent diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 7d31e291667..ca09fbbbcc9 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,11 +1,15 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config' import * as utils from '../../../src/utils' import {server} from '../../mocks/xhr' describe('Taboola Adapter', function () { let sandbox, hasLocalStorage, cookiesAreEnabled, getDataFromLocalStorage, localStorageIsEnabled, getCookie, commonBidRequest; + const COOKIE_KEY = 'trc_cookie_storage'; + const TGID_COOKIE_KEY = 't_gid'; + const TGID_PT_COOKIE_KEY = 't_pt_gid'; + const TBLA_ID_COOKIE_KEY = 'tbla_id'; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -113,6 +117,50 @@ describe('Taboola Adapter', function () { }); }); + describe('onTimeout', function () { + it('onTimeout exist as a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should send timeout', function () { + const timeoutData = [{ + bidder: 'taboola', + bidId: 'da43860a-4644-442a-b5e0-93f268cf8d19', + params: [{ + publisherId: 'publisherId' + }], + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c' + }] + spec.onTimeout(timeoutData); + expect(server.requests[0].method).to.equal('POST'); + expect(server.requests[0].url).to.equal(EVENT_ENDPOINT + '/timeout'); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal(timeoutData); + }); + }); + + describe('onBidderError', function () { + it('onBidderError exist as a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should send bidder error', function () { + const error = { + status: 204, + statusText: 'No Content' + }; + const bidderRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId' + } + } + spec.onBidderError({error, bidderRequest}); + expect(server.requests[0].method).to.equal('POST'); + expect(server.requests[0].url).to.equal(EVENT_ENDPOINT + '/bidError'); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal({error, bidderRequest}); + }); + }); + describe('buildRequests', function () { const defaultBidRequest = { ...createBidRequest(), @@ -129,10 +177,10 @@ describe('Taboola Adapter', function () { } it('should build display request', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); const expectedData = { - id: 'mock-uuid', 'imp': [{ - 'id': 1, + 'id': res.data.imp[0].id, 'banner': { format: [{ w: displayBidRequestParams.sizes[0][0], @@ -149,6 +197,8 @@ describe('Taboola Adapter', function () { 'bidfloorcur': 'USD', 'ext': {} }], + id: 'mock-uuid', + 'test': 0, 'site': { 'id': commonBidRequest.params.publisherId, 'name': commonBidRequest.params.publisherId, @@ -168,13 +218,15 @@ describe('Taboola Adapter', function () { 'ext': {}, }, 'regs': {'coppa': 0, 'ext': {}}, - 'ext': {} + 'ext': { + 'prebid': { + 'version': '$prebid.version$' + } + } }; - const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); - - expect(res.url).to.equal(`${END_POINT_URL}/${commonBidRequest.params.publisherId}`); - expect(res.data).to.deep.equal(JSON.stringify(expectedData)); + expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); + expect(JSON.stringify(res.data)).to.deep.equal(JSON.stringify(expectedData)); }); it('should pass optional parameters in request', function () { @@ -189,9 +241,8 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(0.25); - expect(resData.imp[0].bidfloorcur).to.deep.equal('EUR'); + expect(res.data.imp[0].bidfloor).to.deep.equal(0.25); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('EUR'); }); it('should pass bid floor', function () { @@ -206,9 +257,8 @@ describe('Taboola Adapter', function () { } }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(2.7); - expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); + expect(res.data.imp[0].bidfloor).to.deep.equal(2.7); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('USD'); }); it('should pass bid floor even if it is a bid floor param', function () { @@ -228,9 +278,8 @@ describe('Taboola Adapter', function () { } }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(2.7); - expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); + expect(res.data.imp[0].bidfloor).to.deep.equal(2.7); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('USD'); }); it('should pass impression position', function () { @@ -244,8 +293,7 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].banner.pos).to.deep.equal(2); + expect(res.data.imp[0].banner.pos).to.deep.equal(2); }); it('should pass gpid if configured', function () { @@ -261,8 +309,23 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + expect(res.data.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + }); + + it('should pass new parameter to imp ext', function () { + const ortb2Imp = { + ext: { + example: 'example' + } + } + const bidRequest = { + ...defaultBidRequest, + ortb2Imp: ortb2Imp, + params: {...commonBidRequest.params} + }; + + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.example).to.deep.equal('example'); }); it('should pass bidder timeout', function () { @@ -271,8 +334,25 @@ describe('Taboola Adapter', function () { timeout: 500 } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.tmax).to.equal(500); + expect(res.data.tmax).to.equal(500); + }); + + it('should pass bidder tmax as int', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: '500' + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.tmax).to.equal(500); + }); + + it('should pass bidder timeout as null', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: null + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.tmax).to.equal(undefined); }); describe('first party data', function () { @@ -286,10 +366,9 @@ describe('Taboola Adapter', function () { } } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.bcat).to.deep.equal(bidderRequest.ortb2.bcat) - expect(resData.badv).to.deep.equal(bidderRequest.ortb2.badv) - expect(resData.wlang).to.deep.equal(bidderRequest.ortb2.wlang) + expect(res.data.bcat).to.deep.equal(bidderRequest.ortb2.bcat) + expect(res.data.badv).to.deep.equal(bidderRequest.ortb2.badv) + expect(res.data.wlang).to.deep.equal(bidderRequest.ortb2.wlang) }); it('should pass pageType if exists in ortb2', function () { @@ -304,8 +383,44 @@ describe('Taboola Adapter', function () { } } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + expect(res.data.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + }); + + it('should pass additional parameter in request', function () { + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + ext: { + example: 'example' + } + } + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.ext.example).to.deep.equal(bidderRequest.ortb2.ext.example); + }); + + it('should pass additional parameter in request for topics', function () { + const ortb2 = { + ...commonBidderRequest, + ortb2: { + user: { + data: { + segment: [ + { + id: '243' + } + ], + name: 'pa.taboola.com', + ext: { + segclass: '4', + segtax: 601 + } + } + } + } + } + const res = spec.buildRequests([defaultBidRequest], {...ortb2}) + expect(res.data.user.data).to.deep.equal(ortb2.ortb2.user.data); }); }); @@ -322,9 +437,8 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([defaultBidRequest], bidderRequest) - const resData = JSON.parse(res.data) - expect(resData.user.ext.consent).to.equal('consentString') - expect(resData.regs.ext.gdpr).to.equal(1) + expect(res.data.user.ext.consent).to.equal('consentString') + expect(res.data.regs.ext.gdpr).to.equal(1) }); it('should pass GPP consent if exist in ortb2', function () { @@ -336,9 +450,8 @@ describe('Taboola Adapter', function () { } const res = spec.buildRequests([defaultBidRequest], {...commonBidderRequest, ortb2}) - const resData = JSON.parse(res.data) - expect(resData.regs.ext.gpp).to.equal('testGpp') - expect(resData.regs.ext.gpp_sid).to.deep.equal([1, 2, 3]) + expect(res.data.regs.ext.gpp).to.equal('testGpp') + expect(res.data.regs.ext.gpp_sid).to.deep.equal([1, 2, 3]) }); it('should pass us privacy consent', function () { @@ -349,16 +462,14 @@ describe('Taboola Adapter', function () { uspConsent: 'consentString' } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.regs.ext.us_privacy).to.equal('consentString'); + expect(res.data.regs.ext.us_privacy).to.equal('consentString'); }); it('should pass coppa consent', function () { config.setConfig({coppa: true}) const res = spec.buildRequests([defaultBidRequest], commonBidderRequest) - const resData = JSON.parse(res.data); - expect(resData.regs.coppa).to.equal(1) + expect(res.data.regs.coppa).to.equal(1) config.resetConfig() }); @@ -375,8 +486,7 @@ describe('Taboola Adapter', function () { timeout: 500 } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(51525152); + expect(res.data.user.buyeruid).to.equal(51525152); }); it('should get user id from cookie if local storage isn`t defined', function () { @@ -390,9 +500,106 @@ describe('Taboola Adapter', function () { ...commonBidderRequest }; const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); + expect(res.data.user.buyeruid).to.equal('12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, only TGID_COOKIE_KEY exists', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === COOKIE_KEY) { + return 'should:not:return:this'; + } + if (cookieKey === TGID_COOKIE_KEY) { + return 'user:12121212'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('user:12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, only TGID_PT_COOKIE_KEY exists', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === TGID_PT_COOKIE_KEY) { + return 'user:12121212'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('user:12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, only TBLA_ID_COOKIE_KEY exists', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === TBLA_ID_COOKIE_KEY) { + return 'user:tbla:12121212'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('user:tbla:12121212'); + }); + + it('should get user id from cookie if local storage isn`t defined, all cookie keys exist', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.callsFake(function (cookieKey) { + if (cookieKey === COOKIE_KEY) { + return 'taboola%20global%3Auser-id=cookie:1'; + } + if (cookieKey === TGID_COOKIE_KEY) { + return 'cookie:2'; + } + if (cookieKey === TGID_PT_COOKIE_KEY) { + return 'cookie:3'; + } + if (cookieKey === TBLA_ID_COOKIE_KEY) { + return 'cookie:4'; + } + return undefined; + }); + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.user.buyeruid).to.equal('cookie:1'); + }); + + it('should get user id from tgid cookie if local storage isn`t defined', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.returns('d966c5be-c49f-4f73-8cd1-37b6b5790653-tuct9f7bf10'); + + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); - expect(resData.user.buyeruid).to.equal('12121212'); + expect(res.data.user.buyeruid).to.equal('d966c5be-c49f-4f73-8cd1-37b6b5790653-tuct9f7bf10'); }); it('should get user id from TRC if local storage and cookie isn`t defined', function () { @@ -408,8 +615,7 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(window.TRC.user_id); + expect(res.data.user.buyeruid).to.equal(window.TRC.user_id); delete window.TRC; }); @@ -422,8 +628,7 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(0); + expect(res.data.user.buyeruid).to.equal(0); }); it('should set buyeruid to be 0 if it`s a new user', function () { @@ -431,13 +636,29 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(0); + expect(res.data.user.buyeruid).to.equal(0); }); }); }) describe('interpretResponse', function () { + const defaultBidRequest = { + ...createBidRequest(), + ...displayBidRequestParams, + }; + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + const bidderRequest = { + ...commonBidderRequest + }; + const request = spec.buildRequests([defaultBidRequest], bidderRequest); + const serverResponse = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -446,7 +667,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': request.data.imp[0].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', @@ -471,14 +692,180 @@ describe('Taboola Adapter', function () { } }; - const request = { - bids: [ - { - ...commonBidRequest, - ...displayBidRequestParams + const serverResponseWithPa = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': request.data.imp[0].id, + 'price': 0.342068, + 'adid': '2785119545551083381', + 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/', + + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD', + 'ext': { + 'igbid': [ + { + 'impid': request.data.imp[0].id, + 'igbuyer': [ + { + 'origin': 'https://pa.taboola.com', + 'buyerdata': '{\"seller\":\"pa.taboola.com\",\"resolveToConfig\":false,\"perBuyerSignals\":{\"https://pa.taboola.com\":{\"country\":\"US\",\"route\":\"AM\",\"cct\":[0.02241223,-0.8686833,0.96153843],\"vct\":\"-1967600173\",\"ccv\":null,\"ect\":[-0.13584597,2.5825605],\"ri\":\"100fb73d4064bc\",\"vcv\":\"165229814\",\"ecv\":[-0.39882636,-0.05216012],\"publisher\":\"test-headerbidding\",\"platform\":\"DESK\"}},\"decisionLogicUrl\":\"https://pa.taboola.com/score/decisionLogic.js\",\"sellerTimeout\":100,\"interestGroupBuyers\":[\"https://pa.taboola.com\"],\"perBuyerTimeouts\":{\"*\":50}}' + } + ] + } + ] } - ] - } + } + }; + + const serverResponseWithPartialPa = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': request.data.imp[0].id, + 'price': 0.342068, + 'adid': '2785119545551083381', + 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/', + + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD', + 'ext': { + 'igbid': [ + { + 'impid': request.data.imp[0].id, + 'igbuyer': [ + { + 'origin': 'https://pa.taboola.com', + 'buyerdata': '{}' + } + ] + } + ] + } + } + }; + + const serverResponseWithWrongPa = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': request.data.imp[0].id, + 'price': 0.342068, + 'adid': '2785119545551083381', + 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/', + + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD', + 'ext': { + 'igbid': [ + { + 'impid': request.data.imp[0].id, + 'igbuyer': [ + { + } + ] + } + ] + } + } + }; + + const serverResponseWithEmptyIgbidWIthWrongPa = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': request.data.imp[0].id, + 'price': 0.342068, + 'adid': '2785119545551083381', + 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/', + + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD', + 'ext': { + 'igbid': [ + { + } + ] + } + } + }; it('should return empty array if no valid bids', function () { const res = spec.interpretResponse(serverResponse, []) @@ -513,18 +900,7 @@ describe('Taboola Adapter', function () { }); it('should interpret multi impression request', function () { - const multiRequest = { - bids: [ - { - ...createBidRequest(), - ...displayBidRequestParams - }, - { - ...createBidRequest(), - ...displayBidRequestParams - } - ] - } + const multiRequest = spec.buildRequests([defaultBidRequest, defaultBidRequest], bidderRequest); const multiServerResponse = { body: { @@ -534,7 +910,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '2', + 'impid': multiRequest.data.imp[0].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': 'ADM2', @@ -551,7 +927,7 @@ describe('Taboola Adapter', function () { }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': multiRequest.data.imp[1].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': 'ADM1', @@ -582,6 +958,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[1].bidId, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponse.body.seatbid[0].bid[0].id, ttl: 60, netRevenue: true, currency: multiServerResponse.body.cur, @@ -598,6 +976,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[0].bidId, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponse.body.seatbid[0].bid[1].id, ttl: 60, netRevenue: true, currency: multiServerResponse.body.cur, @@ -621,8 +1001,10 @@ describe('Taboola Adapter', function () { const expectedRes = [ { requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, ttl: 60, netRevenue: true, currency: serverResponse.body.cur, @@ -641,6 +1023,181 @@ describe('Taboola Adapter', function () { expect(res).to.deep.equal(expectedRes) }); + it('should interpret display response with PA', function () { + const [bid] = serverResponse.body.seatbid[0].bid; + + const expectedRes = { + 'bids': [ + { + requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, + cpm: bid.price, + creativeId: bid.crid, + creative_id: bid.crid, + ttl: 60, + netRevenue: true, + currency: serverResponse.body.cur, + mediaType: 'banner', + ad: bid.adm, + width: bid.w, + height: bid.h, + nurl: 'http://win.example.com/', + meta: { + 'advertiserDomains': bid.adomain + }, + } + ], + 'fledgeAuctionConfigs': [ + { + 'impId': request.bids[0].bidId, + 'config': { + 'seller': 'pa.taboola.com', + 'resolveToConfig': false, + 'sellerSignals': {}, + 'sellerTimeout': 100, + 'perBuyerSignals': { + 'https://pa.taboola.com': { + 'country': 'US', + 'route': 'AM', + 'cct': [ + 0.02241223, + -0.8686833, + 0.96153843 + ], + 'vct': '-1967600173', + 'ccv': null, + 'ect': [ + -0.13584597, + 2.5825605 + ], + 'ri': '100fb73d4064bc', + 'vcv': '165229814', + 'ecv': [ + -0.39882636, + -0.05216012 + ], + 'publisher': 'test-headerbidding', + 'platform': 'DESK' + } + }, + 'auctionSignals': {}, + 'decisionLogicUrl': 'https://pa.taboola.com/score/decisionLogic.js', + 'interestGroupBuyers': [ + 'https://pa.taboola.com' + ], + 'perBuyerTimeouts': { + '*': 50 + } + } + } + ] + } + + const res = spec.interpretResponse(serverResponseWithPa, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response with partialPA', function () { + const [bid] = serverResponse.body.seatbid[0].bid; + const expectedRes = { + 'bids': [ + { + requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, + cpm: bid.price, + creativeId: bid.crid, + creative_id: bid.crid, + ttl: 60, + netRevenue: true, + currency: serverResponse.body.cur, + mediaType: 'banner', + ad: bid.adm, + width: bid.w, + height: bid.h, + nurl: 'http://win.example.com/', + meta: { + 'advertiserDomains': bid.adomain + }, + } + ], + 'fledgeAuctionConfigs': [ + { + 'impId': request.bids[0].bidId, + 'config': { + 'seller': undefined, + 'resolveToConfig': undefined, + 'sellerSignals': {}, + 'sellerTimeout': undefined, + 'perBuyerSignals': {}, + 'auctionSignals': {}, + 'decisionLogicUrl': undefined, + 'interestGroupBuyers': undefined, + 'perBuyerTimeouts': undefined + } + } + ] + } + + const res = spec.interpretResponse(serverResponseWithPartialPa, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response with wrong PA', function () { + const [bid] = serverResponse.body.seatbid[0].bid; + + const expectedRes = [ + { + requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, + cpm: bid.price, + creativeId: bid.crid, + creative_id: bid.crid, + ttl: 60, + netRevenue: true, + currency: serverResponse.body.cur, + mediaType: 'banner', + ad: bid.adm, + width: bid.w, + height: bid.h, + nurl: 'http://win.example.com/', + meta: { + 'advertiserDomains': bid.adomain + }, + } + ] + + const res = spec.interpretResponse(serverResponseWithWrongPa, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response with empty igbid wrong PA', function () { + const [bid] = serverResponse.body.seatbid[0].bid; + + const expectedRes = [ + { + requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, + cpm: bid.price, + creativeId: bid.crid, + creative_id: bid.crid, + ttl: 60, + netRevenue: true, + currency: serverResponse.body.cur, + mediaType: 'banner', + ad: bid.adm, + width: bid.w, + height: bid.h, + nurl: 'http://win.example.com/', + meta: { + 'advertiserDomains': bid.adomain + }, + } + ] + + const res = spec.interpretResponse(serverResponseWithEmptyIgbidWIthWrongPa, request) + expect(res).to.deep.equal(expectedRes) + }); + it('should set the correct ttl form the response', function () { // set exp-ttl to be 125 const [bid] = serverResponse.body.seatbid[0].bid; @@ -648,8 +1205,10 @@ describe('Taboola Adapter', function () { const expectedRes = [ { requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, ttl: 125, netRevenue: true, currency: serverResponse.body.cur, @@ -668,18 +1227,7 @@ describe('Taboola Adapter', function () { }); it('should replace AUCTION_PRICE macro in adm', function () { - const multiRequest = { - bids: [ - { - ...createBidRequest(), - ...displayBidRequestParams - }, - { - ...createBidRequest(), - ...displayBidRequestParams - } - ] - } + const multiRequest = spec.buildRequests([defaultBidRequest, defaultBidRequest], bidderRequest); const multiServerResponseWithMacro = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -688,7 +1236,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '2', + 'impid': multiRequest.data.imp[0].id, 'price': 0.34, 'adid': '2785119545551083381', 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', @@ -705,7 +1253,7 @@ describe('Taboola Adapter', function () { }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': multiRequest.data.imp[1].id, 'price': 0.35, 'adid': '2785119545551083381', 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', @@ -735,6 +1283,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[1].bidId, cpm: multiServerResponseWithMacro.body.seatbid[0].bid[0].price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponseWithMacro.body.seatbid[0].bid[0].id, ttl: 60, netRevenue: true, currency: multiServerResponseWithMacro.body.cur, @@ -751,6 +1301,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[0].bidId, cpm: multiServerResponseWithMacro.body.seatbid[0].bid[1].price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponseWithMacro.body.seatbid[0].bid[1].id, ttl: 60, netRevenue: true, currency: multiServerResponseWithMacro.body.cur, @@ -771,17 +1323,28 @@ describe('Taboola Adapter', function () { describe('getUserSyncs', function () { const usersyncUrl = 'https://trc.taboola.com/sg/prebidJS/1/cm'; + const iframeUrl = 'https://cdn.taboola.com/scripts/prebid_iframe_sync.html'; - it('should not return user sync if pixelEnabled is false', function () { - const res = spec.getUserSyncs({pixelEnabled: false}); + it('should not return user sync if pixelEnabled is false and iframe disabled', function () { + const res = spec.getUserSyncs({pixelEnabled: false, iframeEnabled: false}); expect(res).to.be.an('array').that.is.empty; }); it('should return user sync if pixelEnabled is true', function () { - const res = spec.getUserSyncs({pixelEnabled: true}); + const res = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: false}); expect(res).to.deep.equal([{type: 'image', url: usersyncUrl}]); }); + it('should return user sync if iframeEnabled is true', function () { + const res = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}); + expect(res).to.deep.equal([{type: 'iframe', url: iframeUrl}]); + }); + + it('should return both user syncs if iframeEnabled is true and pixelEnabled is true', function () { + const res = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}); + expect(res).to.deep.equal([{type: 'iframe', url: iframeUrl}, {type: 'image', url: usersyncUrl}]); + }); + it('should pass consent tokens values', function() { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'GDPR_CONSENT'}, 'USP_CONSENT')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT` @@ -795,8 +1358,11 @@ describe('Taboola Adapter', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', 'GPP_STRING')).to.deep.equal([{ - type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING` + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', {gppString: 'GPP_STRING', applicableSections: []})).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', {gppString: 'GPP_STRING', applicableSections: [32, 51]})).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING&gpp_sid=32%2C51` }]); }); }) diff --git a/test/spec/modules/tagorasBidAdapter_spec.js b/test/spec/modules/tagorasBidAdapter_spec.js new file mode 100644 index 00000000000..7559567dcff --- /dev/null +++ b/test/spec/modules/tagorasBidAdapter_spec.js @@ -0,0 +1,651 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/tagorasBidAdapter'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppString': 'gpp_string', + 'gppSid': [7], + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['tagoras.io'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('TagorasBidAdapter', function () { + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + tagoras: { + storageAllowed: true + } + }; + sandbox = sinon.sandbox.create(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + prebidVersion: version, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + }, + gpid: '' + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.tagoras.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.tagoras.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.tagoras.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }); + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.tagoras.io/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['tagoras.io'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['tagoras.io'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['tagoras.io'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'lipb': + return {lipbid: id}; + case 'parrableId': + return {eid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + tagoras: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + tagoras: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem('myKey', 2020); + const {value, created} = getStorageItem('myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem('myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 58a62fb2869..46fac8de1e2 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -466,4 +466,23 @@ describe('Tappx bid adapter', function () { assert.isString(_extractPageUrl(validBidRequests, bidderRequest)); }); }) + + describe('Empty params values from bid tests', function() { + let validBidRequest = JSON.parse(JSON.stringify(c_BIDREQUEST)); + + it('should return false when tappxkey is empty', function () { + validBidRequest.bids[0].params.tappxkey = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when host is empty', function () { + validBidRequest.bids[0].params.host = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when endpoint is empty', function () { + validBidRequest.bids[0].params.endpoint = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + }); }); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 0771ac62e67..81e09b09d08 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -1,23 +1,21 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/teadsBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; +import * as autoplay from 'libraries/autoplayDetection/autoplay.js' const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; describe('teadsBidAdapter', () => { const adapter = newBidder(spec); - let cookiesAreEnabledStub, getCookieStub; + let sandbox; beforeEach(function () { - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); + sandbox = sinon.sandbox.create(); }); afterEach(function () { - cookiesAreEnabledStub.restore(); - getCookieStub.restore(); + sandbox.restore(); }); describe('inherited functions', () => { @@ -257,6 +255,193 @@ describe('teadsBidAdapter', () => { expect(payload.pageReferrer).to.deep.equal(document.referrer); }); + it('should add width info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceWidth = screen.width + + expect(payload.deviceWidth).to.exist; + expect(payload.deviceWidth).to.deep.equal(deviceWidth); + }); + + it('should add height info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceHeight = screen.height + + expect(payload.deviceHeight).to.exist; + expect(payload.deviceHeight).to.deep.equal(deviceHeight); + }); + + it('should add pixelRatio info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const pixelRatio = window.top.devicePixelRatio + + expect(payload.devicePixelRatio).to.exist; + expect(payload.devicePixelRatio).to.deep.equal(pixelRatio); + }); + + it('should add screenOrientation info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const screenOrientation = window.top.screen.orientation?.type + + if (screenOrientation) { + expect(payload.screenOrientation).to.exist; + expect(payload.screenOrientation).to.deep.equal(screenOrientation); + } else expect(payload.screenOrientation).to.not.exist; + }); + + it('should add historyLength info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add hardwareConcurrency info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const hardwareConcurrency = window.top.navigator?.hardwareConcurrency + + if (hardwareConcurrency) { + expect(payload.hardwareConcurrency).to.exist; + expect(payload.hardwareConcurrency).to.deep.equal(hardwareConcurrency); + } else expect(payload.hardwareConcurrency).to.not.exist + }); + + it('should add deviceMemory info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceMemory = window.top.navigator.deviceMemory + + if (deviceMemory) { + expect(payload.deviceMemory).to.exist; + expect(payload.deviceMemory).to.deep.equal(deviceMemory); + } else expect(payload.deviceMemory).to.not.exist; + }); + + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + }); + it('should add timeToFirstByte info to payload', function () { const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); @@ -681,7 +866,7 @@ describe('teadsBidAdapter', () => { describe('First-party cookie Teads ID', function () { it('should not add firstPartyCookieTeadsId param to payload if cookies are not enabled' + ' and teads user id not available', function () { - cookiesAreEnabledStub.returns(false); + sandbox.stub(storage, 'cookiesAreEnabled').returns(false); const bidRequest = { ...baseBidRequest, @@ -698,8 +883,8 @@ describe('teadsBidAdapter', () => { it('should not add firstPartyCookieTeadsId param to payload if cookies are enabled ' + 'but first-party cookie and teads user id are not available', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns(undefined); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns(undefined); const bidRequest = { ...baseBidRequest, @@ -716,8 +901,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from cookie if it\'s available ' + 'and teads user id is not', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -735,8 +920,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from user id module if it\'s available ' + 'even if cookie is available too', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -855,6 +1040,45 @@ describe('teadsBidAdapter', () => { } }); } + + it('should add dsa info to payload if available', function () { + const bidRequestWithDsa = Object.assign({}, bidderRequestDefault, { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + } + } + } + }); + + const requestWithDsa = spec.buildRequests(bidRequests, bidRequestWithDsa); + const payload = JSON.parse(requestWithDsa.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa).to.deep.equal( + { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + ); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).dsa).to.not.exist; + }); }); describe('interpretResponse', function() { @@ -870,7 +1094,8 @@ describe('teadsBidAdapter', () => { 'ttl': 360, 'width': 300, 'creativeId': 'er2ee', - 'placementId': 34 + 'placementId': 34, + 'needAutoplay': true }, { 'ad': AD_SCRIPT, 'cpm': 0.5, @@ -881,7 +1106,19 @@ describe('teadsBidAdapter', () => { 'width': 350, 'creativeId': 'fs3ff', 'placementId': 34, - 'dealId': 'ABC_123' + 'needAutoplay': false, + 'dealId': 'ABC_123', + 'ext': { + 'dsa': { + 'behalf': 'some-behalf', + 'paid': 'some-paid', + 'transparency': [{ + 'domain': 'test.com', + 'dsaparams': [1, 2, 3] + }], + 'adrender': 1 + } + } }] } }; @@ -907,7 +1144,16 @@ describe('teadsBidAdapter', () => { 'currency': 'USD', 'netRevenue': true, 'meta': { - advertiserDomains: [] + advertiserDomains: [], + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + } }, 'ttl': 360, 'ad': AD_SCRIPT, @@ -923,6 +1169,70 @@ describe('teadsBidAdapter', () => { expect(result).to.eql(expectedResponse); }); + it('should filter bid responses with needAutoplay:true when autoplay is disabled', function() { + let bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'bidId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee', + 'placementId': 34, + 'needAutoplay': true + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 200, + 'bidId': '4fef3b4gb1ec15', + 'ttl': 360, + 'width': 350, + 'creativeId': 'fs3ff', + 'placementId': 34, + 'needAutoplay': false + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.7, + 'currency': 'USD', + 'height': 600, + 'bidId': 'a987fbc961d', + 'ttl': 12, + 'width': 300, + 'creativeId': 'awuygfd', + 'placementId': 12, + 'needAutoplay': true + }] + } + }; + let expectedResponse = [{ + 'cpm': 0.5, + 'width': 350, + 'height': 200, + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [], + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '4fef3b4gb1ec15', + 'creativeId': 'fs3ff', + 'placementId': 34 + } + ] + ; + + const isAutoplayEnabledStub = sinon.stub(autoplay, 'isAutoplayEnabled'); + isAutoplayEnabledStub.returns(false); + let result = spec.interpretResponse(bids); + isAutoplayEnabledStub.restore(); + expect(result).to.eql(expectedResponse); + }); + it('handles nobid responses', function() { let bids = { 'body': { diff --git a/test/spec/modules/teadsIdSystem_spec.js b/test/spec/modules/teadsIdSystem_spec.js index 7b977e2fb2b..1959b990957 100644 --- a/test/spec/modules/teadsIdSystem_spec.js +++ b/test/spec/modules/teadsIdSystem_spec.js @@ -218,7 +218,7 @@ describe('TeadsIdSystem', function () { callback(callbackSpy); const request = server.requests[0]; expect(request.url).to.include(teadsUrl); - request.respond(204, null, 'Unavailable'); + request.respond(204); expect(logInfoStub.calledOnce).to.be.true; }); diff --git a/test/spec/modules/theAdxBidAdapter_spec.js b/test/spec/modules/theAdxBidAdapter_spec.js index 99e5156190c..eb00834421a 100644 --- a/test/spec/modules/theAdxBidAdapter_spec.js +++ b/test/spec/modules/theAdxBidAdapter_spec.js @@ -446,6 +446,78 @@ describe('TheAdxAdapter', function () { expect(processedBid.currency).to.equal(responseCurrency); }); + it('returns a valid deal bid response on sucessful banner request with deal', function () { + let incomingRequestId = 'XXtestingXX'; + let responsePrice = 3.14 + + let responseCreative = 'sample_creative&{FOR_COVARAGE}'; + + let responseCreativeId = '274'; + let responseCurrency = 'TRY'; + + let responseWidth = 300; + let responseHeight = 250; + let responseTtl = 213; + let dealId = 'theadx_deal_id'; + + let sampleResponse = { + id: '66043f5ca44ecd8f8769093b1615b2d9', + seatbid: [{ + bid: [{ + id: 'c21bab0e-7668-4d8f-908a-63e094c09197', + dealid: 'theadx_deal_id', + impid: '1', + price: responsePrice, + adid: responseCreativeId, + crid: responseCreativeId, + adm: responseCreative, + adomain: [ + 'www.domain.com' + ], + cid: '274', + attr: [], + w: responseWidth, + h: responseHeight, + ext: { + ttl: responseTtl + } + }], + seat: '201', + group: 0 + }], + bidid: 'c21bab0e-7668-4d8f-908a-63e094c09197', + cur: responseCurrency + }; + + let sampleRequest = { + bidId: incomingRequestId, + mediaTypes: { + banner: {} + }, + requestId: incomingRequestId, + deals: [{id: dealId}] + }; + let serverResponse = { + body: sampleResponse + } + let result = spec.interpretResponse(serverResponse, sampleRequest); + + expect(result.length).to.equal(1); + + let processedBid = result[0]; + + // expect(processedBid.requestId).to.equal(incomingRequestId); + expect(processedBid.cpm).to.equal(responsePrice); + expect(processedBid.width).to.equal(responseWidth); + expect(processedBid.height).to.equal(responseHeight); + expect(processedBid.ad).to.equal(responseCreative); + expect(processedBid.ttl).to.equal(responseTtl); + expect(processedBid.creativeId).to.equal(responseCreativeId); + expect(processedBid.netRevenue).to.equal(true); + expect(processedBid.currency).to.equal(responseCurrency); + expect(processedBid.dealId).to.equal(dealId); + }); + it('returns an valid bid response on sucessful video request', function () { let incomingRequestId = 'XXtesting-275XX'; let responsePrice = 6 diff --git a/test/spec/modules/themoneytizerBidAdapter_spec.js b/test/spec/modules/themoneytizerBidAdapter_spec.js new file mode 100644 index 00000000000..8cff7a57e69 --- /dev/null +++ b/test/spec/modules/themoneytizerBidAdapter_spec.js @@ -0,0 +1,289 @@ +import { spec } from '../../../modules/themoneytizerBidAdapter.js' + +const ENDPOINT_URL = 'https://ads.biddertmz.com/m/'; + +const VALID_BID_BANNER = { + bidder: 'themoneytizer', + ortb2Imp: { + ext: {} + }, + params: { + pid: 123456, + }, + mediaTypes: { + banner: { + sizes: [[970, 250]] + } + }, + adUnitCode: 'ad-unit-code', + bidId: '82376dbe72be72', + timeout: 3000, + ortb2: {}, + userIdAsEids: [], + auctionId: '123456-abcdef-7890', + schain: {}, +} + +const VALID_TEST_BID_BANNER = { + bidder: 'themoneytizer', + ortb2Imp: { + ext: {} + }, + params: { + pid: 123456, + test: 1, + baseUrl: 'https://custom-endpoint.biddertmz.com/m/' + }, + mediaTypes: { + banner: { + sizes: [[970, 250]] + } + }, + adUnitCode: 'ad-unit-code', + bidId: '82376dbe72be72', + timeout: 3000, + ortb2: {}, + userIdAsEids: [], + auctionId: '123456-abcdef-7890', + schain: {} +} + +const BIDDER_REQUEST_BANNER = { + bids: [VALID_BID_BANNER, VALID_TEST_BID_BANNER], + refererInfo: { + topmostLocation: 'http://prebid.org/', + canonicalUrl: 'http://prebid.org/' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'abcdefghxyz' + } +} + +const SERVER_RESPONSE = { + c_sync: { + status: 'ok', + bidder_status: [ + { + bidder: 'bidder-A', + usersync: { + url: 'https://syncurl.com', + type: 'redirect' + } + }, + { + bidder: 'bidder-B', + usersync: { + url: 'https://syncurl2.com', + type: 'image' + } + } + ] + }, + bid: { + requestId: '17750222eb16825', + cpm: 0.098, + currency: 'USD', + width: 300, + height: 600, + creativeId: '44368852571075698202250', + dealId: '', + netRevenue: true, + ttl: 5, + ad: '

This is an ad

', + mediaType: 'banner', + } +}; + +describe('The Moneytizer Bidder Adapter', function () { + describe('codes', function () { + it('should return a bidder code of themoneytizer', function () { + expect(spec.code).to.equal('themoneytizer'); + }); + }); + + describe('gvlid', function () { + it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(1265) + }); + }); + + describe('isBidRequestValid', function () { + it('should return true for a bid with all required fields', function () { + const validBid = spec.isBidRequestValid(VALID_BID_BANNER); + expect(validBid).to.be.true; + }); + + it('should return false for an invalid bid', function () { + const invalidBid = spec.isBidRequestValid(null); + expect(invalidBid).to.be.false; + }); + + it('should return false when params are incomplete', function () { + const bidWithIncompleteParams = { + ...VALID_BID_BANNER, + params: {} + }; + expect(spec.isBidRequestValid(bidWithIncompleteParams)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let requests, request, requests_test, request_test; + + before(function () { + requests = spec.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + request = requests[0]; + + requests_test = spec.buildRequests([VALID_TEST_BID_BANNER], BIDDER_REQUEST_BANNER); + request_test = requests_test[0]; + }); + + it('should build a request array for valid bids', function () { + expect(requests).to.be.an('array').that.is.not.empty; + }); + + it('should build a request array for valid test bids', function () { + expect(requests_test).to.be.an('array').that.is.not.empty; + }); + + it('should build a request with the correct method, URL, and data type', function () { + expect(request).to.include.keys(['method', 'url', 'data']); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT_URL); + expect(request.data).to.be.a('string'); + }); + + it('should build a test request with the correct method, URL, and data type', function () { + expect(request_test).to.include.keys(['method', 'url', 'data']); + expect(request_test.method).to.equal('POST'); + expect(request_test.url).to.equal(VALID_TEST_BID_BANNER.params.baseUrl); + expect(request_test.data).to.be.a('string'); + }); + + describe('Payload structure', function () { + let payload; + + before(function () { + payload = JSON.parse(request.data); + }); + + it('should have correct payload structure', function () { + expect(payload).to.be.an('object'); + expect(payload.size).to.be.an('object'); + expect(payload.params).to.be.an('object'); + }); + }); + + describe('Payload structure optional params', function () { + let payload; + + before(function () { + payload = JSON.parse(request_test.data); + }); + + it('should have correct params', function () { + expect(payload.params.pid).to.equal(123456); + }); + + it('should have correct referer info', function () { + expect(payload.referer).to.equal(BIDDER_REQUEST_BANNER.refererInfo.topmostLocation); + expect(payload.referer_canonical).to.equal(BIDDER_REQUEST_BANNER.refererInfo.canonicalUrl); + }); + + it('should have correct GDPR consent', function () { + expect(payload.consent_string).to.equal(BIDDER_REQUEST_BANNER.gdprConsent.consentString); + expect(payload.consent_required).to.equal(BIDDER_REQUEST_BANNER.gdprConsent.gdprApplies); + }); + }); + }); + + describe('interpretResponse', function () { + let bidResponse, receivedBid; + const responseBody = SERVER_RESPONSE; + + before(function () { + receivedBid = responseBody.bid; + const response = { body: responseBody }; + bidResponse = spec.interpretResponse(response, null); + }); + + it('should not return an empty response', function () { + expect(bidResponse).to.not.be.empty; + }); + + describe('Parsed Bid Object', function () { + let bid; + + before(function () { + bid = bidResponse[0]; + }); + + it('should not be empty', function () { + expect(bid).to.not.be.empty; + }); + + it('should correctly interpret ad markup', function () { + expect(bid.ad).to.equal(receivedBid.ad); + }); + + it('should correctly interpret CPM', function () { + expect(bid.cpm).to.equal(receivedBid.cpm); + }); + + it('should correctly interpret dimensions', function () { + expect(bid.height).to.equal(receivedBid.height); + expect(bid.width).to.equal(receivedBid.width); + }); + + it('should correctly interpret request ID', function () { + expect(bid.requestId).to.equal(receivedBid.requestId); + }); + }); + }); + + describe('onTimeout', function () { + const timeoutData = [{ + timeout: null + }]; + + it('should exists and be a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should include timeoutData', function () { + expect(spec.onTimeout(timeoutData)).to.be.undefined; + }) + }); + + describe('getUserSyncs', function () { + const response = { body: SERVER_RESPONSE }; + + it('should have empty user sync with iframeEnabled to false and pixelEnabled to false', function () { + const result = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: false }, [response]); + + expect(result).to.be.empty; + }); + + it('should have user sync with iframeEnabled to true', function () { + const result = spec.getUserSyncs({ iframeEnabled: true }, [response]); + + expect(result).to.not.be.empty; + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal(SERVER_RESPONSE.c_sync.bidder_status[0].usersync.url); + }); + + it('should have user sync with pixelEnabled to true', function () { + const result = spec.getUserSyncs({ pixelEnabled: true }, [response]); + + expect(result).to.not.be.empty; + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal(SERVER_RESPONSE.c_sync.bidder_status[0].usersync.url); + }); + + it('should transform type redirect into image', function () { + const result = spec.getUserSyncs({ iframeEnabled: true }, [response]); + + expect(result[1].type).to.equal('image'); + }); + }); +}); diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index 22d7a98d45d..4a79e7f77fd 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -1,289 +1,313 @@ import { + getCachedTopics, getTopics, getTopicsData, + loadTopicsForBidders, processFpd, - hasGDPRConsent, - getCachedTopics, receiveMessage, + reset, topicStorageName } from '../../../modules/topicsFpdModule.js'; +import {config} from 'src/config.js'; import {deepClone, safeJSONParse} from '../../../src/utils.js'; -import {gdprDataHandler} from 'src/adapterManager.js'; import {getCoreStorageManager} from 'src/storageManager.js'; +import * as activities from '../../../src/activities/rules.js'; +import {ACTIVITY_ENRICH_UFPD} from '../../../src/activities/activities.js'; -describe('getTopicsData', () => { - function makeTopic(topic, modelv, taxv = '1') { - return { - topic, - taxonomyVersion: taxv, - modelVersion: modelv - }; - } - - function byTaxClass(segments) { - return segments.reduce((memo, segment) => { - memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; - return memo; - }, {}); - } - - [ - { - t: 'no topics', - topics: [], - expected: [] - }, - { - t: 'single topic', - topics: [makeTopic(123, 'm1')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'} - ] - } - ] - }, - { - t: 'multiple topics with the same model version', - topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'}, - {id: '321'} - ] - } - ] - }, - { - t: 'multiple topics with different model versions', - topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '1'}, - {id: '2'} - ] - }, - { - ext: { - segtax: 600, - segclass: 'm2' - }, - segment: [ - {id: '3'} - ] - } - ] - }, - { - t: 'multiple topics, some with a taxonomy version other than "1"', - topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'} - ] - } - ] - }, - { - t: 'multiple topics in multiple taxonomies', - taxonomies: { - '1': 600, - '2': 601 +describe('topics', () => { + beforeEach(() => { + reset(); + }); + + describe('getTopicsData', () => { + function makeTopic(topic, modelv, taxv = '1') { + return { + topic, + taxonomyVersion: taxv, + modelVersion: modelv + }; + } + + function byTaxClass(segments) { + return segments.reduce((memo, segment) => { + memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; + return memo; + }, {}); + } + + [ + { + t: 'no topics', + topics: [], + expected: [] }, - topics: [ - makeTopic(123, 'm1', '1'), - makeTopic(321, 'm1', '2'), - makeTopic(213, 'm2', '1'), - ], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' + { + t: 'single topic', + topics: [makeTopic(123, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics with the same model version', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'}, + {id: '321'} + ] + } + ] + }, + { + t: 'multiple topics with different model versions', + topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '1'}, + {id: '2'} + ] }, - segment: [ - {id: '123'} - ] + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '3'} + ] + } + ] + }, + { + t: 'multiple topics, some with a taxonomy version other than "1"', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics in multiple taxonomies', + taxonomies: { + '1': 600, + '2': 601 }, - { - ext: { - segtax: 601, - segclass: 'm1', + topics: [ + makeTopic(123, 'm1', '1'), + makeTopic(321, 'm1', '2'), + makeTopic(213, 'm2', '1'), + ], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] }, - segment: [ - {id: '321'} - ] - }, - { - ext: { - segtax: 600, - segclass: 'm2' + { + ext: { + segtax: 601, + segclass: 'm1', + }, + segment: [ + {id: '321'} + ] }, - segment: [ - {id: '213'} - ] - } - ] - } - ].forEach(({t, topics, expected, taxonomies}) => { - describe(`on ${t}`, () => { - it('should convert topics to user.data segments correctly', () => { - const actual = getTopicsData('mockName', topics, taxonomies); - expect(actual.length).to.eql(expected.length); - expected = byTaxClass(expected); - Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { - sinon.assert.match(datum, expected[key]); - expect(datum.name).to.equal('mockName'); + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '213'} + ] + } + ] + } + ].forEach(({t, topics, expected, taxonomies}) => { + describe(`on ${t}`, () => { + it('should convert topics to user.data segments correctly', () => { + const actual = getTopicsData('mockName', topics, taxonomies); + expect(actual.length).to.eql(expected.length); + expected = byTaxClass(expected); + Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { + sinon.assert.match(datum, expected[key]); + expect(datum.name).to.equal('mockName'); + }); }); - }); - it('should not set name if null', () => { - getTopicsData(null, topics).forEach((data) => { - expect(data.hasOwnProperty('name')).to.be.false; + it('should not set name if null', () => { + getTopicsData(null, topics).forEach((data) => { + expect(data.hasOwnProperty('name')).to.be.false; + }); }); }); }); }); -}); -describe('getTopics', () => { - Object.entries({ - 'document with no browsingTopics': {}, - 'document that disallows topics': { - featurePolicy: { - allowsFeature: sinon.stub().returns(false) - } - }, - 'document that throws on featurePolicy': { - browsingTopics: sinon.stub(), - get featurePolicy() { - throw new Error(); - } - }, - 'document that throws on browsingTopics': { - browsingTopics: sinon.stub().callsFake(() => { - throw new Error(); - }), - featurePolicy: { - allowsFeature: sinon.stub().returns(true) - } - }, - }).forEach(([t, doc]) => { - it(`should resolve to an empty list on ${t}`, () => { - return getTopics(doc).then((topics) => { - expect(topics).to.eql([]); + describe('getTopics', () => { + Object.entries({ + 'document with no browsingTopics': {}, + 'document that disallows topics': { + featurePolicy: { + allowsFeature: sinon.stub().returns(false) + } + }, + 'document that throws on featurePolicy': { + browsingTopics: sinon.stub(), + get featurePolicy() { + throw new Error(); + } + }, + 'document that throws on browsingTopics': { + browsingTopics: sinon.stub().callsFake(() => { + throw new Error(); + }), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }, + }).forEach(([t, doc]) => { + it(`should resolve to an empty list on ${t}`, () => { + return getTopics(doc).then((topics) => { + expect(topics).to.eql([]); + }); }); }); - }); - it('should call `document.browsingTopics` when allowed', () => { - const topics = ['t1', 't2']; - return getTopics({ - browsingTopics: sinon.stub().returns(Promise.resolve(topics)), - featurePolicy: { - allowsFeature: sinon.stub().returns(true) - } - }).then((actual) => { - expect(actual).to.eql(topics); - }); - }); -}); - -describe('processFpd', () => { - const mockData = [ - { - name: 'domain', - segment: [{id: 123}] - }, - { - name: 'domain', - segment: [{id: 321}] - } - ]; - - it('should add topics data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - expect(global.user.data).to.eql(mockData); + it('should call `document.browsingTopics` when allowed', () => { + const topics = ['t1', 't2']; + return getTopics({ + browsingTopics: sinon.stub().returns(Promise.resolve(topics)), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }).then((actual) => { + expect(actual).to.eql(topics); }); + }); }); - it('should apppend to existing user.data', () => { - const global = { - user: { - data: [ - {name: 'preexisting'}, - ] + describe('processFpd', () => { + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}] } - }; - return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) - .then((data) => { - expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); - }); - }); - - it('should not modify fpd when there is no data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve([])}) - .then((data) => { - expect(data.global).to.eql({}); - }); - }); -}); + ]; -describe('Topics Module GDPR consent check', () => { - let gdprDataHdlrStub; - beforeEach(() => { - gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); - }); + it('should add topics data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + expect(global.user.data).to.eql(mockData); + }); + }); - afterEach(() => { - gdprDataHdlrStub.restore(); - }); + it('should apppend to existing user.data', () => { + const global = { + user: { + data: [ + {name: 'preexisting'}, + ] + } + }; + return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) + .then((data) => { + expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); + }); + }); - it('should return false when GDPR is applied but consent string is not present', () => { - const consentString = ''; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: {} - }; - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(false); + it('should not modify fpd when there is no data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve([])}) + .then((data) => { + expect(data.global).to.eql({}); + }); + }); }); - it('should return true when GDPR doesn\'t apply', () => { - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: false, - vendorData: {} - }; + describe('loadTopicsForBidders', () => { + beforeEach(() => { + config.setConfig({ + userSync: { + topics: { + bidders: [{ + bidder: 'mockBidder', + iframeURL: 'https://mock.iframe' + }] + } + } + }) + }); + afterEach(() => { + config.resetConfig(); + }) - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(true); + Object.entries({ + 'support': {}, + 'allow': { + browsingTopics: true, + featurePolicy: { + allowsFeature(feature) { + return feature !== 'browsing-topics'; + } + } + }, + }).forEach(([t, doc]) => { + it(`does not attempt to load frames if browser does not ${t} topics`, () => { + doc.createElement = sinon.stub(); + loadTopicsForBidders(doc); + sinon.assert.notCalled(doc.createElement); + }); + }); }); - it('should return true when GDPR is applied and purpose consent is true for all purpose[1,2,3,4]', () => { + describe('getCachedTopics()', () => { + const storage = getCoreStorageManager('topicsFpd'); + const expected = [{ + ext: { + segtax: 600, + segclass: '2206021246' + }, + segment: [{ + 'id': '243' + }, { + 'id': '265' + }], + name: 'ads.pubmatic.com' + }]; const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; const consentConfig = { consentString: consentString, @@ -301,98 +325,182 @@ describe('Topics Module GDPR consent check', () => { } } }; + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}], + } + ]; - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(true); - }); + const evt = { + data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', + origin: 'https://ads.pubmatic.com' + }; - it('should return false when GDPR is applied and purpose consent is false for one of the purpose[1,2,3,4]', () => { - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true, - purpose: { - consents: { - 1: true, - 2: true, - 3: true, - 4: false + afterEach(() => { + storage.removeDataFromLocalStorage(topicStorageName); + }); + + describe('when cached data is available and not expired', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + const storedSegments = JSON.stringify( + [['pubmatic', { + '2206021246': { + 'ext': {'segtax': 600, 'segclass': '2206021246'}, + 'segment': [{'id': '243'}, {'id': '265'}], + 'name': 'ads.pubmatic.com' + }, + 'lastUpdated': new Date().getTime() + }]] + ); + storage.setDataInLocalStorage(topicStorageName, storedSegments); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return segments for bidder if transmitUfpd is allowed', () => { + assert.deepEqual(getCachedTopics(), expected); + }); + + it('should NOT return segments for bidder if enrichUfpd is NOT allowed', () => { + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity, params) => { + return !(activity === ACTIVITY_ENRICH_UFPD && params.component === 'bidder.pubmatic'); + }); + expect(getCachedTopics()).to.eql([]); + }); + }) + + it('should return empty segments for bidder if there is cached segments stored which is expired', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + assert.deepEqual(getCachedTopics(), []); + }); + + describe('cross-frame messages', () => { + before(() => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html' + } + ], + }, } - } - } - }; + }); + }); + + beforeEach(() => { + // init iframe logic so that the receiveMessage origin check passes + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + }, + createElement: sinon.stub().callsFake(() => ({style: {}})), + documentElement: { + appendChild() {} + } + }); + }); + + after(() => { + config.resetConfig(); + }) - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(false); + it('should store segments if receiveMessage event is triggered with segment data', () => { + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.has('pubmatic')).to.equal(true); + }); + + it('should update stored segments if receiveMessage event is triggerred with segment data', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); + }); + }); }); }); -describe('getCachedTopics()', () => { +describe('handles fetch request for topics api headers', () => { + let stubbedFetch; const storage = getCoreStorageManager('topicsFpd'); - const expected = [{ - ext: { - segtax: 600, - segclass: '2206021246' - }, - segment: [{ - 'id': '243' - }, { - 'id': '265' - }], - name: 'ads.pubmatic.com' - }]; - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true, - purpose: { - consents: { - 1: true, - 2: true, - 3: true, - 4: true - } - } - } - }; - const mockData = [ - { - name: 'domain', - segment: [{id: 123}] - }, - { - name: 'domain', - segment: [{id: 321}], - } - ]; - - const evt_pm = { - data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', - origin: 'https://ads.pubmatic.com' - }; - const evt_rh = { - data: '{"segment":{"domain":"topics.authorizedvault.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"rtbhouse"},"date":1669743901858}', - origin: 'https://topics.authorizedvault.com' - }; - - let gdprDataHdlrStub; beforeEach(() => { - gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); + stubbedFetch = sinon.stub(window, 'fetch'); + reset(); }); afterEach(() => { + stubbedFetch.restore(); storage.removeDataFromLocalStorage(topicStorageName); - gdprDataHdlrStub.restore(); + config.resetConfig(); + }); + + it('should make a fetch call when a fetchUrl is present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js' + } + ], + }, + } + }); + + stubbedFetch.returns(Promise.resolve(true)); + + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.calledOnce(stubbedFetch); + stubbedFetch.calledWith('http://localhost:3000/topics-server.js'); }); - it('should return segments for bidder if GDPR consent is true and there is cached segments stored which is not expired', () => { + it('should not make a fetch call when a fetchUrl is not present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic' + } + ], + }, + } + }); + + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); + }); + + it('a fetch request should not be made if the configured fetch rate duration has not yet passed', () => { const storedSegments = JSON.stringify( [['pubmatic', { '2206021246': { @@ -403,36 +511,30 @@ describe('getCachedTopics()', () => { 'lastUpdated': new Date().getTime() }]] ); - storage.setDataInLocalStorage(topicStorageName, storedSegments); - gdprDataHdlrStub.returns(consentConfig); - assert.deepEqual(getCachedTopics(), expected); - }); - it('should return empty segments for bidder if GDPR consent is true and there is cached segments stored which is expired', () => { - let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; storage.setDataInLocalStorage(topicStorageName, storedSegments); - gdprDataHdlrStub.returns(consentConfig); - assert.deepEqual(getCachedTopics(), []); - }); - it('should stored segments if receiveMessage event is triggerred with segment data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - receiveMessage(evt_pm); - receiveMessage(evt_rh); - let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); - expect(segments.has('pubmatic') || segments.has('rtbhouse')).to.equal(true); - }); - }); + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js', + fetchRate: 1 // in days. 1 fetch per day + } + ], + }, + } + }); - it('should update stored segments if receiveMessage event is triggerred with segment data', () => { - let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; - storage.setDataInLocalStorage(topicStorageName, storedSegments); - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - receiveMessage(evt_pm); - let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); - expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); - }); + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); }); }); diff --git a/test/spec/modules/tpmnBidAdapter_spec.js b/test/spec/modules/tpmnBidAdapter_spec.js index e2b14b18f61..505bc9d878f 100644 --- a/test/spec/modules/tpmnBidAdapter_spec.js +++ b/test/spec/modules/tpmnBidAdapter_spec.js @@ -1,16 +1,130 @@ /* eslint-disable no-tabs */ -import {expect} from 'chai'; -import {spec, storage} from 'modules/tpmnBidAdapter.js'; -import {generateUUID} from '../../../src/utils.js'; -import {newBidder} from '../../../src/adapters/bidderFactory'; +import { spec, storage, VIDEO_RENDERER_URL, ADAPTER_VERSION } from 'modules/tpmnBidAdapter.js'; +import { generateUUID } from '../../../src/utils.js'; +import { expect } from 'chai'; +import * as utils from 'src/utils'; import * as sinon from 'sinon'; +import 'modules/consentManagement.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {mockGdprConsent} from '../../helpers/consentData.js'; + +const BIDDER_CODE = 'tpmn'; +const BANNER_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const VIDEO_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + video: { + context: 'outstream', + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + playerSize: [[1024, 768]], + protocols: [3, 4, 7, 8, 10], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const BIDDER_REQUEST = { + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + bidderRequestId: 'bidderRequestId', + timeout: 500, + refererInfo: { + page: 'https://hello-world-page.com/', + domain: 'hello-world-page.com', + ref: 'http://example-domain.com/foo', + } +}; + +const BANNER_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidId': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'w': 300, + 'h': 250 + } + ] + } + ], + 'cur': 'USD' +}; + +const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 1.09, + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 768, + 'w': 1024 + } + ] + } + ], + 'cur': 'USD' +}; describe('tpmnAdapterTests', function () { - const adapter = newBidder(spec); - const BIDDER_CODE = 'tpmn'; let sandbox = sinon.sandbox.create(); let getCookieStub; - beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { tpmn: { @@ -27,152 +141,277 @@ describe('tpmnAdapterTests', function () { $$PREBID_GLOBAL$$.bidderSettings = {}; }); - 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 = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - } - }; - - it('should return true if a bid is valid banner bid request', function () { - expect(spec.isBidRequestValid(bid)).to.be.equal(true); - }); - - it('should return false where requried param is missing', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - - it('should return false when required param values have invalid type', function () { - let bid = Object.assign({}, bid); - bid.params = { - 'inventoryId': null, - 'publisherId': null - }; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - }); - - describe('buildRequests', function () { - it('should return an empty list if there are no bid requests', function () { - const emptyBidRequests = []; - const bidderRequest = {}; - const request = spec.buildRequests(emptyBidRequests, bidderRequest); - expect(request).to.be.an('array').that.is.empty; - }); - it('should generate a POST server request with bidder API url, data', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', + describe('isBidRequestValid()', function () { + it('should accept request if placementId is passed', function () { + let bid = { + bidder: BIDDER_CODE, params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 123 }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', mediaTypes: { banner: { sizes: [[300, 250]] } } }; - const tempBidRequests = [bid]; - const tempBidderRequest = { - refererInfo: { - page: 'http://localhost/test', - site: { - domain: 'localhost', - page: 'http://localhost/test' - } - } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should reject requests without params', function () { + let bid = { + bidder: BIDDER_CODE, + params: {} }; - const builtRequest = spec.buildRequests(tempBidRequests, tempBidderRequest); - - expect(builtRequest).to.have.lengthOf(1); - expect(builtRequest[0].method).to.equal('POST'); - expect(builtRequest[0].url).to.match(/ad.tpmn.co.kr\/prebidhb.tpmn/); - const apiRequest = builtRequest[0].data; - expect(apiRequest.site).to.deep.equal({ - domain: 'localhost', - page: 'http://localhost/test' - }); - expect(apiRequest.bids).to.have.lengthOf('1'); - expect(apiRequest.bids[0].inventoryId).to.equal('1'); - expect(apiRequest.bids[0].publisherId).to.equal('TPMN'); - expect(apiRequest.bids[0].bidId).to.equal('29092404798c9'); - expect(apiRequest.bids[0].adUnitCode).to.equal('temp-unitcode'); - expect(apiRequest.bids[0].auctionId).to.equal('da1d7a33-0260-4e83-a621-14674116f3f9'); - expect(apiRequest.bids[0].sizes).to.have.lengthOf('1'); - expect(apiRequest.bids[0].sizes[0]).to.deep.equal({ - width: 300, - height: 250 - }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(BANNER_BID)).to.equal(true); + expect(spec.isBidRequestValid(VIDEO_BID)).to.equal(true); }); }); - describe('interpretResponse', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] + describe('buildRequests()', function () { + it('should have gdpr data if applicable', function () { + const bid = utils.deepClone(BANNER_BID); + + const req = syncAddFPDToBidderRequest(Object.assign({}, BIDDER_REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, } + })); + let request = spec.buildRequests([bid], req)[0]; + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + + it('should properly forward ORTB blocking params', function () { + let bid = utils.deepClone(BANNER_BID); + bid = utils.mergeDeep(bid, { + params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'], battr: [1] }, + mediaTypes: { banner: { battr: [1] } } + }); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + + expect(request).to.exist.and.to.be.an('object'); + const payload = request.data; + expect(payload).to.have.deep.property('bcat', ['IAB1-1']); + expect(payload).to.have.deep.property('badv', ['example.com']); + expect(payload).to.have.deep.property('bapp', ['com.example']); + expect(payload.imp[0].banner).to.have.deep.property('battr', [1]); + }); + + context('when mediaType is banner', function () { + it('should build correct request for banner bid with both w, h', () => { + const bid = utils.deepClone(BANNER_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const requestData = request.data; + // expect(requestData.imp[0].banner).to.equal(null); + expect(requestData.imp[0].banner.format[0].w).to.equal(300); + expect(requestData.imp[0].banner.format[0].h).to.equal(250); + }); + + it('should create request data', function () { + const bid = utils.deepClone(BANNER_BID); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bid.bidId); + }); + }); + + context('when mediaType is video', function () { + if (FEATURES.VIDEO) { + it('should return false when there is no video in mediaTypes', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); } - }; - const tempBidRequests = [bid]; - it('should return an empty aray to indicate no valid bids', function () { - const emptyServerResponse = {}; - const bidResponses = spec.interpretResponse(emptyServerResponse, tempBidRequests); - expect(bidResponses).is.an('array').that.is.empty; + if (FEATURES.VIDEO) { + it('should reutrn false if player size is not set', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + } + if (FEATURES.VIDEO) { + it('when mediaType is Video - check', () => { + const bid = utils.deepClone(VIDEO_BID); + const check = { + w: 1024, + h: 768, + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + api: [1, 2, 4, 6], + protocols: [3, 4, 7, 8, 10], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0, + plcmt: 1 + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } + + if (FEATURES.VIDEO) { + it('when mediaType New Video', () => { + const NEW_VIDEO_BID = { + 'bidder': 'tpmn', + 'params': {'inventoryId': 2, 'bidFloor': 2}, + 'userId': {'pubcid': '88a49ee6-beeb-4dd6-92ac-3b6060e127e1'}, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': ['video/mp4'], + 'playerSize': [[1024, 768]], + 'playbackmethod': [2, 4, 6], + 'protocols': [3, 4], + 'api': [1, 2, 3, 6], + 'placement': 1, + 'minduration': 0, + 'maxduration': 30, + 'startdelay': 0, + 'skip': 1, + 'plcmt': 4 + } + }, + }; + + const check = { + w: 1024, + h: 768, + mimes: [ 'video/mp4' ], + playbackmethod: [2, 4, 6], + api: [1, 2, 3, 6], + protocols: [3, 4], + placement: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + skip: 1, + plcmt: 4 + } + + expect(spec.isBidRequestValid(NEW_VIDEO_BID)).to.equal(true); + let requests = spec.buildRequests([NEW_VIDEO_BID], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video.w).to.equal(check.w); + expect(request.imp[0].video.h).to.equal(check.h); + expect(request.imp[0].video.placement).to.equal(check.placement); + expect(request.imp[0].video.minduration).to.equal(check.minduration); + expect(request.imp[0].video.maxduration).to.equal(check.maxduration); + expect(request.imp[0].video.startdelay).to.equal(check.startdelay); + expect(request.imp[0].video.skip).to.equal(check.skip); + expect(request.imp[0].video.plcmt).to.equal(check.plcmt); + expect(request.imp[0].video.mimes).to.deep.have.same.members(check.mimes); + expect(request.imp[0].video.playbackmethod).to.deep.have.same.members(check.playbackmethod); + expect(request.imp[0].video.api).to.deep.have.same.members(check.api); + expect(request.imp[0].video.protocols).to.deep.have.same.members(check.protocols); + }); + } + + if (FEATURES.VIDEO) { + it('should use bidder video params if they are set', () => { + let bid = utils.deepClone(VIDEO_BID); + const check = { + api: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [3, 4], + protocols: [5, 6], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + w: 640, + h: 480 + + }; + bid.mediaTypes.video = {...check}; + bid.mediaTypes.video.context = 'instream'; + bid.mediaTypes.video.playerSize = [[640, 480]]; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } }); - it('should return an empty array to indicate no valid bids', function () { - const mockBidResult = { - requestId: '9cf19229-34f6-4d06-bc1d-0e44e8d616c8', - cpm: 10.0, - creativeId: '1', - width: 300, - height: 250, - netRevenue: true, - currency: 'USD', - ttl: 1800, - ad: '', - adType: 'banner' - }; - const testServerResponse = { - headers: [], - body: [mockBidResult] - }; - const bidResponses = spec.interpretResponse(testServerResponse, tempBidRequests); - expect(bidResponses).deep.equal([mockBidResult]); + }); + + describe('interpretResponse()', function () { + context('when mediaType is banner', function () { + it('should correctly interpret valid banner response', function () { + const bid = utils.deepClone(BANNER_BID); + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const response = utils.deepClone(BANNER_BID_RESPONSE); + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[0].burl).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].ad).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].creativeId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + + it('should handle empty bid response', function () { + const bid = utils.deepClone(BANNER_BID); + + let request = spec.buildRequests([bid], BIDDER_REQUEST)[0]; + const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); }); + if (FEATURES.VIDEO) { + context('when mediaType is video', function () { + it('should correctly interpret valid instream video response', () => { + const bid = utils.deepClone(VIDEO_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].burl).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].vastXml).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].rendererUrl).to.equal(VIDEO_RENDERER_URL); + expect(bids[0].creativeId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + }); + } }); describe('getUserSync', function () { diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 70f6b43eef6..851425574d0 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -736,258 +736,83 @@ describe('triplelift adapter', function () { }); it('should add tdid to the payload if included', function () { - const id = '6bca7f6b-a98a-46c0-be05-6020f7604598'; - bidRequests[0].userId.tdid = id; - const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; - expect(payload).to.exist; - expect(payload.user).to.deep.equal({ext: {eids: [{source: 'adserver.org', uids: [{id, ext: {rtiPartner: 'TDID'}}]}]}}); - }); - - it('should add idl_env to the payload if included', function () { - const id = 'XY6104gr0njcH9UDIR7ysFFJcm2XNpqeJTYslleJ_cMlsFOfZI'; - bidRequests[0].userId.idl_env = id; + const tdid = '6bca7f6b-a98a-46c0-be05-6020f7604598'; + bidRequests[0].userIdAsEids = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID' + }, + id: tdid + } + ] + }, + ]; const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const payload = request.data; expect(payload).to.exist; - expect(payload.user).to.deep.equal({ext: {eids: [{source: 'liveramp.com', uids: [{id, ext: {rtiPartner: 'idl'}}]}]}}); + expect(payload.user).to.deep.equal({ext: {eids: [{source: 'adserver.org', uids: [{id: tdid, atype: 1, ext: {rtiPartner: 'TDID'}}]}]}}); }); it('should add criteoId to the payload if included', function () { const id = '53e30ea700424f7bbdd793b02abc5d7'; - bidRequests[0].userId.criteoId = id; - const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; - expect(payload).to.exist; - expect(payload.user).to.deep.equal({ext: {eids: [{source: 'criteo.com', uids: [{id, ext: {rtiPartner: 'criteoId'}}]}]}}); - }); - - it('should add adqueryId to the payload if included', function () { - const id = '%7B%22qid%22%3A%229c985f8cc31d9b3c000d%22%7D'; - bidRequests[0].userIdAsEids = [{ source: 'adquery.io', uids: [{ id }] }]; - const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; - expect(payload).to.exist; - expect(payload.user).to.deep.equal({ext: {eids: [{source: 'adquery.io', uids: [{id, ext: {rtiPartner: 'adquery.io'}}]}]}}); - }); - - it('should add amxRtbId to the payload if included', function () { - const id = 'Ok9JQkBM-UFlAXEZQ-UUNBQlZOQzgrUFhW-UUNBQkRQTUBPQVpVWVxNXlZUUF9AUFhAUF9PXFY/'; - bidRequests[0].userIdAsEids = [{ source: 'amxdt.net', uids: [{ id }] }]; - const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; - expect(payload).to.exist; - expect(payload.user).to.deep.equal({ext: {eids: [{source: 'amxdt.net', uids: [{id, ext: {rtiPartner: 'amxdt.net'}}]}]}}); - }); - - it('should add tdid, idl_env and criteoId to the payload if both are included', function () { - const tdidId = '6bca7f6b-a98a-46c0-be05-6020f7604598'; - const idlEnvId = 'XY6104gr0njcH9UDIR7ysFFJcm2XNpqeJTYslleJ_cMlsFOfZI'; - const criteoId = '53e30ea700424f7bbdd793b02abc5d7'; - bidRequests[0].userId.tdid = tdidId; - bidRequests[0].userId.idl_env = idlEnvId; - bidRequests[0].userId.criteoId = criteoId; - - const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; - - expect(payload).to.exist; - expect(payload.user).to.deep.equal({ - ext: { - eids: [ - { - source: 'adserver.org', - uids: [ - { - id: tdidId, - ext: { rtiPartner: 'TDID' } - } - ], - }, - { - source: 'liveramp.com', - uids: [ - { - id: idlEnvId, - ext: { rtiPartner: 'idl' } - } - ] - }, + bidRequests[0].userIdAsEids = [ + { + source: 'criteo.com', + uids: [ { - source: 'criteo.com', - uids: [ - { - id: criteoId, - ext: { rtiPartner: 'criteoId' } - } - ] + atype: 1, + ext: { + rtiPartner: 'criteoId' + }, + id: id } ] - } - }); - }); - - it('should consolidate user ids from multiple bid requests', function () { - const tdidId = '6bca7f6b-a98a-46c0-be05-6020f7604598'; - const idlEnvId = 'XY6104gr0njcH9UDIR7ysFFJcm2XNpqeJTYslleJ_cMlsFOfZI'; - const criteoId = '53e30ea700424f7bbdd793b02abc5d7'; - const pubcid = '3261d8ad-435d-481d-abd1-9f1a9ec99f0e'; - - const bidRequestsMultiple = [ - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } } + }, ]; - - const request = tripleliftAdapterSpec.buildRequests(bidRequestsMultiple, bidderRequest); + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const payload = request.data; - - expect(payload.user).to.deep.equal({ - ext: { - eids: [ - { - source: 'adserver.org', - uids: [ - { - id: tdidId, - ext: { rtiPartner: 'TDID' } - } - ], - }, - { - source: 'liveramp.com', - uids: [ - { - id: idlEnvId, - ext: { rtiPartner: 'idl' } - } - ] - }, - { - source: 'criteo.com', - uids: [ - { - id: criteoId, - ext: { rtiPartner: 'criteoId' } - } - ] - }, - { - source: 'pubcid.org', - uids: [ - { - id: '3261d8ad-435d-481d-abd1-9f1a9ec99f0e', - ext: { rtiPartner: 'pubcid' } - } - ] - } - ] - } - }); - - expect(payload.user.ext.eids).to.be.an('array'); - expect(payload.user.ext.eids).to.have.lengthOf(4); + expect(payload).to.exist; + expect(payload.user).to.deep.equal({ext: {eids: [{source: 'criteo.com', uids: [{id: id, atype: 1, ext: {rtiPartner: 'criteoId'}}]}]}}); }); - it('should remove malformed ids that would otherwise break call', function () { - let tdidId = '6bca7f6b-a98a-46c0-be05-6020f7604598'; - let idlEnvId = null; // fail; can't be null - let criteoId = '53e30ea700424f7bbdd793b02abc5d7'; - let pubcid = ''; // fail; can't be empty string - - let bidRequestsMultiple = [ - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } } - ]; - - let request = tripleliftAdapterSpec.buildRequests(bidRequestsMultiple, bidderRequest); - let payload = request.data; - - expect(payload.user).to.deep.equal({ - ext: { - eids: [ - { - source: 'adserver.org', - uids: [ - { - id: tdidId, - ext: { rtiPartner: 'TDID' } - } - ], - }, + it('should add tdid and criteoId to the payload if both are included', function () { + const tdid = '6bca7f6b-a98a-46c0-be05-6020f7604598'; + const criteoId = '53e30ea700424f7bbdd793b02abc5d7'; + bidRequests[0].userIdAsEids = [ + { + source: 'adserver.org', + uids: [ { - source: 'criteo.com', - uids: [ - { - id: criteoId, - ext: { rtiPartner: 'criteoId' } - } - ] + atype: 1, + ext: { + rtiPartner: 'TDID' + }, + id: tdid } ] - } - }); - - expect(payload.user.ext.eids).to.be.an('array'); - expect(payload.user.ext.eids).to.have.lengthOf(2); - - tdidId = {}; // fail; can't be empty object - idlEnvId = { id: '987654' }; // pass - criteoId = [{ id: '123456' }]; // fail; can't be an array - pubcid = '3261d8ad-435d-481d-abd1-9f1a9ec99f0e'; // pass - - bidRequestsMultiple = [ - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } } - ]; - - request = tripleliftAdapterSpec.buildRequests(bidRequestsMultiple, bidderRequest); - payload = request.data; - - expect(payload.user).to.deep.equal({ - ext: { - eids: [ - { - source: 'liveramp.com', - uids: [ - { - id: '987654', - ext: { rtiPartner: 'idl' } - } - ] - }, + }, + { + source: 'criteo.com', + uids: [ { - source: 'pubcid.org', - uids: [ - { - id: pubcid, - ext: { rtiPartner: 'pubcid' } - } - ] + atype: 1, + ext: { + rtiPartner: 'criteoId' + }, + id: criteoId } ] - } - }); - - expect(payload.user.ext.eids).to.be.an('array'); - expect(payload.user.ext.eids).to.have.lengthOf(2); - - tdidId = { id: '987654' }; // pass - idlEnvId = { id: 987654 }; // fail; can't be an int - criteoId = '53e30ea700424f7bbdd793b02abc5d7'; // pass - pubcid = { id: '' }; // fail; can't be an empty string - - bidRequestsMultiple = [ - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, - { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } } + }, ]; - request = tripleliftAdapterSpec.buildRequests(bidRequestsMultiple, bidderRequest); - payload = request.data; + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + expect(payload).to.exist; expect(payload.user).to.deep.equal({ ext: { eids: [ @@ -995,7 +820,8 @@ describe('triplelift adapter', function () { source: 'adserver.org', uids: [ { - id: '987654', + id: tdid, + atype: 1, ext: { rtiPartner: 'TDID' } } ], @@ -1005,6 +831,7 @@ describe('triplelift adapter', function () { uids: [ { id: criteoId, + atype: 1, ext: { rtiPartner: 'criteoId' } } ] @@ -1130,6 +957,46 @@ describe('triplelift adapter', function () { expect(logErrorSpy.calledOnce).to.equal(true); }); + it('should add ortb2 ext object if global fpd is available', function() { + const ortb2 = { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + }, + user: { + yob: 1985, + gender: 'm', + keywords: 'a,b', + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 4 }, + segment: [{ id: '1' }] + } + ], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + }; + + const request = tripleliftAdapterSpec.buildRequests(bidRequests, {...bidderRequest, ortb2}); + const { data: payload } = request; + expect(payload.ext.ortb2).to.exist; + expect(payload.ext.ortb2.site).to.deep.equal({ + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + }); + }); it('should send global config fpd if kvps are available', function() { const sens = null; const category = ['news', 'weather', 'hurricane']; @@ -1508,6 +1375,57 @@ describe('triplelift adapter', function () { expect(result[2].meta.networkId).to.equal('5989'); expect(result[3].meta.networkId).to.equal('5989'); }); + + it('should return fledgeAuctionConfigs if PAAPI response is received', function() { + response.body.paapi = [ + { + imp_id: '0', + auctionConfig: { + seller: 'https://3lift.com', + decisionLogicUrl: 'https://3lift.com/decision_logic.js', + interestGroupBuyers: ['https://some_buyer.com'], + perBuyerSignals: { + 'https://some_buyer.com': { a: 1 } + } + } + }, + { + imp_id: '2', + auctionConfig: { + seller: 'https://3lift.com', + decisionLogicUrl: 'https://3lift.com/decision_logic.js', + interestGroupBuyers: ['https://some_other_buyer.com'], + perBuyerSignals: { + 'https://some_other_buyer.com': { b: 2 } + } + } + } + ]; + + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + + expect(result).to.have.property('bids'); + expect(result).to.have.property('fledgeAuctionConfigs'); + expect(result.fledgeAuctionConfigs.length).to.equal(2); + expect(result.fledgeAuctionConfigs[0].bidId).to.equal('30b31c1838de1e'); + expect(result.fledgeAuctionConfigs[1].bidId).to.equal('73edc0ba8de203'); + expect(result.fledgeAuctionConfigs[0].config).to.deep.equal( + { + 'seller': 'https://3lift.com', + 'decisionLogicUrl': 'https://3lift.com/decision_logic.js', + 'interestGroupBuyers': ['https://some_buyer.com'], + 'perBuyerSignals': { 'https://some_buyer.com': { 'a': 1 } } + } + ); + expect(result.fledgeAuctionConfigs[1].config).to.deep.equal( + { + 'seller': 'https://3lift.com', + 'decisionLogicUrl': 'https://3lift.com/decision_logic.js', + 'interestGroupBuyers': ['https://some_other_buyer.com'], + 'perBuyerSignals': { 'https://some_other_buyer.com': { 'b': 2 } } + } + ); + }); }); describe('getUserSyncs', function() { diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 56c506dea6b..1fe504ba8e8 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -262,6 +262,11 @@ describe('ttdBidAdapter', function () { expect(request.data).to.be.not.null; }); + it('sets bidrequest.id to bidderRequestId', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.id).to.equal('18084284054531'); + }); + it('sets impression id to ad unit\'s bid id', function () { const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; expect(requestBody.imp[0].id).to.equal('243310435309b5'); diff --git a/test/spec/modules/ucfunnelBidAdapter_spec.js b/test/spec/modules/ucfunnelBidAdapter_spec.js index 9bec7229450..998e0db6fe8 100644 --- a/test/spec/modules/ucfunnelBidAdapter_spec.js +++ b/test/spec/modules/ucfunnelBidAdapter_spec.js @@ -30,7 +30,7 @@ const validBannerBidReq = { params: { adid: 'ad-34BBD2AA24B678BBFD4E7B9EE3B872D' }, - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746', ortb2Imp: { @@ -180,15 +180,15 @@ describe('ucfunnel Adapter', function () { expect(data.schain).to.equal('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com'); }); - it('must parse bid size from a nested array', function () { - const width = 640; - const height = 480; - const bid = deepClone(validBannerBidReq); - bid.sizes = [[ width, height ]]; - const requests = spec.buildRequests([ bid ], bidderRequest); + it('should support multiple size', function () { + const sizes = [[300, 250], [336, 280]]; + const format = '300,250;336,280'; + validBannerBidReq.sizes = sizes; + const requests = spec.buildRequests([ validBannerBidReq ], bidderRequest); const data = requests[0].data; - expect(data.w).to.equal(width); - expect(data.h).to.equal(height); + expect(data.w).to.equal(sizes[0][0]); + expect(data.h).to.equal(sizes[0][1]); + expect(data.format).to.equal(format); }); it('should set bidfloor if configured', function() { diff --git a/test/spec/modules/uid2IdSystem_helpers.js b/test/spec/modules/uid2IdSystem_helpers.js index 65d52c1d7c3..e0bef047acb 100644 --- a/test/spec/modules/uid2IdSystem_helpers.js +++ b/test/spec/modules/uid2IdSystem_helpers.js @@ -1,6 +1,6 @@ -import { setConsentConfig } from 'modules/consentManagement.js'; -import { server } from 'test/mocks/xhr.js'; -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {setConsentConfig} from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; +import {coreStorage, requestBidsHook} from 'modules/userId/index.js'; const msIn12Hours = 60 * 60 * 12 * 1000; const expireCookieDate = 'Thu, 01 Jan 1970 00:00:01 GMT'; @@ -26,16 +26,16 @@ export const runAuction = async () => { } export const apiHelpers = { - makeTokenResponse: (token, shouldRefresh = false, expired = false) => ({ + makeTokenResponse: (token, shouldRefresh = false, expired = false, refreshExpired = false) => ({ advertising_token: token, refresh_token: 'fake-refresh-token', identity_expires: expired ? Date.now() - 1000 : Date.now() + 60 * 60 * 1000, refresh_from: shouldRefresh ? Date.now() - 1000 : Date.now() + 60 * 1000, - refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + refresh_expires: refreshExpired ? Date.now() - 1000 : Date.now() + 24 * 60 * 60 * 1000, // 24 hours refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', // Fake data }), - respondAfterDelay: (delay) => new Promise((resolve) => setTimeout(() => { - server.respond(); + respondAfterDelay: (delay, srv = server) => new Promise((resolve) => setTimeout(() => { + srv.respond(); setTimeout(() => resolve()); }, delay)), } diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index 20a38a292bb..901e0c57e32 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -1,17 +1,17 @@ /* eslint-disable no-console */ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; import 'src/prebid.js'; import 'modules/consentManagement.js'; import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; import { configureTimerInterceptors } from 'test/mocks/timers.js'; import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -24,16 +24,22 @@ const auctionDelayMs = 10; const initialToken = `initial-advertising-token`; const legacyToken = 'legacy-advertising-token'; const refreshedToken = 'refreshed-advertising-token'; +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; const legacyConfigParams = {storage: null}; const serverCookieConfigParams = { uid2ServerCookie: publisherCookieName }; const newServerCookieConfigParams = { uid2Cookie: publisherCookieName }; +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } const makeUid2IdentityContainer = (token) => ({uid2: {id: token}}); let useLocalStorage = false; const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'uid2', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}}] }, debug, ...extraSettings }); +const makeOriginalIdentity = (identity, salt = 1) => ({ + identity: utils.cyrb53Hash(identity, salt), + salt +}) const getFromAppropriateStorage = () => { if (useLocalStorage) return coreStorage.getDataFromLocalStorage(moduleCookieName); @@ -47,31 +53,18 @@ const expectGlobalToHaveToken = (token) => expect(getGlobal().getUserIds()).to.d const expectGlobalToHaveNoUid2 = () => expect(getGlobal().getUserIds()).to.not.haveOwnProperty('uid2'); const expectNoLegacyToken = (bid) => expect(bid.userId).to.not.deep.include(makeUid2IdentityContainer(legacyToken)); const expectModuleStorageEmptyOrMissing = () => expect(getFromAppropriateStorage()).to.be.null; -const expectModuleStorageToContain = (initialIdentity, latestIdentity) => { +const expectModuleStorageToContain = (originalAdvertisingToken, latestAdvertisingToken, originalIdentity) => { const cookie = JSON.parse(getFromAppropriateStorage()); - if (initialIdentity) expect(cookie.originalToken.advertising_token).to.equal(initialIdentity); - if (latestIdentity) expect(cookie.latestToken.advertising_token).to.equal(latestIdentity); + if (originalAdvertisingToken) expect(cookie.originalToken.advertising_token).to.equal(originalAdvertisingToken); + if (latestAdvertisingToken) expect(cookie.latestToken.advertising_token).to.equal(latestAdvertisingToken); + if (originalIdentity) expect(cookie.originalIdentity).to.eql(makeOriginalIdentity(Object.values(originalIdentity)[0], cookie.originalIdentity.salt)); } -const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; +const apiUrl = 'https://prod.uidapi.com/v2/token' +const refreshApiUrl = `${apiUrl}/refresh`; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); -const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); -const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); - -// Runs the provided test twice - once with a successful API mock, once with one which returns a server error -const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { - const testFn = only ? it.only : it; - testFn(`API responds successfully: ${testDescription}`, async function() { - configureUid2ApiSuccessResponse(); - await act(true); - }); - testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { - configureUid2ApiFailResponse(); - await act(false); - }); -} +const makeSuccessResponseBody = (responseToken) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: responseToken } })); +const cstgApiUrl = `${apiUrl}/client-generate`; const testCookieAndLocalStorage = (description, test, only = false) => { const describeFn = only ? describe.only : describe; @@ -104,10 +97,18 @@ describe(`UID2 module`, function () { // I've confirmed it's available in Firefox since v34 (it seems to be unavailable on BrowserStack in Firefox v106). if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function () { @@ -116,14 +117,30 @@ describe(`UID2 module`, function () { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); + const configureUid2Response = (apiUrl, httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureUid2ApiSuccessResponse = (apiUrl, responseToken) => configureUid2Response(apiUrl, 200, makeSuccessResponseBody(responseToken)); + const configureUid2ApiFailResponse = (apiUrl) => configureUid2Response(apiUrl, 500, 'Error'); + // Runs the provided test twice - once with a successful API mock, once with one which returns a server error + const testApiSuccessAndFailure = (act, apiUrl, testDescription, failTestDescription, only = false, responseToken = refreshedToken) => { + const testFn = only ? it.only : it; + testFn(`API responds successfully: ${testDescription}`, async function() { + configureUid2ApiSuccessResponse(apiUrl, responseToken); + await act(true); + }); + testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { + configureUid2ApiFailResponse(apiUrl); + await act(false); + }); + } + const getFullTestTitle = (test) => `${test.parent.title ? getFullTestTitle(test.parent) + ' | ' : ''}${test.title}`; + beforeEach(function () { debugOutput(`----------------- START TEST ------------------`); fullTestTitle = getFullTestTitle(this.test.ctx.currentTest); debugOutput(fullTestTitle); testSandbox = sinon.sandbox.create(); testSandbox.stub(utils, 'logWarn'); - init(config); setSubmoduleRegistry([uid2IdSubmodule]); }); @@ -143,7 +160,6 @@ describe(`UID2 module`, function () { } cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); coreStorage.removeDataFromLocalStorage(moduleCookieName); - debugOutput('----------------- END TEST ------------------'); }); @@ -151,13 +167,13 @@ describe(`UID2 module`, function () { it('When no baseUrl is provided in config, the module calls the production endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token})); - expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/v2/token/refresh'); }); it('When a baseUrl is provided in config, the module calls the provided endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token, uid2ApiBase: 'https://operator-integ.uidapi.com'})); - expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/v2/token/refresh'); }); }); @@ -238,7 +254,7 @@ describe(`UID2 module`, function () { cookieHelpers.setPublisherCookie(publisherCookieName, token); config.setConfig(makePrebidConfig(serverCookieConfigParams, extraConfig)); }, - } + }, ] scenarios.forEach(function(scenario) { @@ -247,16 +263,16 @@ describe(`UID2 module`, function () { describe('When the refresh is available in time', function() { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); const bid = await runAuction(); if (apiSucceeds) expectToken(bid, refreshedToken); else expectNoIdentity(bid); - }, 'it should be used in the auction', 'the auction should have no uid2'); + }, refreshApiUrl, 'it should be used in the auction', 'the auction should have no uid2'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); await runAuction(); if (apiSucceeds) { @@ -264,18 +280,18 @@ describe(`UID2 module`, function () { } else { expectModuleStorageEmptyOrMissing(); } - }, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); + }, refreshApiUrl, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); }); describe(`when the response doesn't arrive before the auction timer`, function() { testApiSuccessAndFailure(async function() { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); const bid = await runAuction(); expectNoIdentity(bid); - }, 'it should run the auction'); + }, refreshApiUrl, 'it should run the auction'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); const bid = await runAuction(); expectNoIdentity(bid); @@ -283,7 +299,7 @@ describe(`UID2 module`, function () { await promise; if (apiSucceeds) expectGlobalToHaveToken(refreshedToken); else expectGlobalToHaveNoUid2(); - }, 'it should update the userId after the auction', 'there should be no global identity'); + }, refreshApiUrl, 'it should update the userId after the auction', 'there should be no global identity'); }) describe('and there is a refreshed token in the module cookie', function() { it('the refreshed value from the cookie is used', async function() { @@ -319,13 +335,13 @@ describe(`UID2 module`, function () { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true), {auctionDelay: 0, syncDelay: 1}); }); testApiSuccessAndFailure(async function() { - apiHelpers.respondAfterDelay(10); + apiHelpers.respondAfterDelay(10, server); const bid = await runAuction(); expectToken(bid, initialToken); - }, 'it should not be refreshed before the auction runs'); + }, refreshApiUrl, 'it should not be refreshed before the auction runs'); testApiSuccessAndFailure(async function(success) { - const promise = apiHelpers.respondAfterDelay(1); + const promise = apiHelpers.respondAfterDelay(1, server); await runAuction(); await promise; if (success) { @@ -333,7 +349,7 @@ describe(`UID2 module`, function () { } else { expectModuleStorageToContain(initialToken, initialToken); } - }, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); + }, refreshApiUrl, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); it('it should use the current token in the auction', async function() { const bid = await runAuction(); @@ -342,4 +358,273 @@ describe(`UID2 module`, function () { }); }); }); + + if (FEATURES.UID2_CSTG) { + describe('When CSTG is enabled provided', function () { + const scenarios = [ + { + name: 'email provided in config', + identity: { email: 'test@example.com' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test . test@gmail.com' }, extraConfig)) + }, + { + name: 'phone provided in config', + identity: { phone: '+12345678910' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phone: 'test123' }, extraConfig)) + }, + { + name: 'email hash provided in config', + identity: { email_hash: 'lz3+Rj7IV4X1+Vr1ujkG7tstkxwk5pgkqJ6mXbpOgTs=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: this.identity.email_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: 'test@example.com' }, extraConfig)) + }, + { + name: 'phone hash provided in config', + identity: { phone_hash: 'kVJ+4ilhrqm3HZDDnCQy4niZknvCoM4MkoVzZrQSdJw=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: this.identity.phone_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: '614332222111' }, extraConfig)) + }, + ] + scenarios.forEach(function(scenario) { + describe(`And ${scenario.name}`, function() { + describe(`When invalid identity is provided`, function() { + it('the auction should have no uid2', async function () { + scenario.setInvalidConfig() + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + }); + + describe('When valid identity is provided, and the auction is set to run immediately', function() { + it('it should ignores token provided in config, and the auction should have no uid2', async function() { + scenario.setConfig({ uid2Token: apiHelpers.makeTokenResponse(initialToken), auctionDelay: 0, syncDelay: 1 }); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + it('it should ignores token provided in server-set cookie', async function() { + cookieHelpers.setPublisherCookie(publisherCookieName, initialToken); + scenario.setConfig({ ...newServerCookieConfigParams, auctionDelay: 0, syncDelay: 1 }) + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + describe('When the token generated in time', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should be used in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, scenario.identity); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + }); + describe(`when the response doesn't arrive before the auction timer`, function() { + testApiSuccessAndFailure(async function() { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }, cstgApiUrl, 'it should run the auction', undefined, false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); + + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + await promise; + if (apiSucceeds) expectGlobalToHaveToken(clientSideGeneratedToken); + else expectGlobalToHaveNoUid2(); + }, cstgApiUrl, 'it should update the userId after the auction', 'there should be no global identity', false, clientSideGeneratedToken); + }) + + describe('when there is a token in the module cookie', function() { + describe('when originalIdentity matches', function() { + describe('When the storedToken is valid', function() { + it('it should use the stored token in the auction', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com', auctionDelay: 0, syncDelay: 1 })); + const bid = await runAuction(); + expectToken(bid, refreshedToken); + }); + }) + + describe('When the storedToken is expired and can be refreshed ', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, refreshedToken); + else expectNoIdentity(bid); + }, refreshApiUrl, 'it should use refreshed token in the auction', 'the auction should have no uid2'); + }) + + describe('When the storedToken is expired for refresh', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should use generated token in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + }) + }) + + it('when originalIdentity not match, the auction should has no uid2', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }); + }) + }); + describe('When invalid CSTG configuration is provided', function () { + const invalidConfigs = [ + { + name: 'CSTG option is not a object', + cstgOptions: '' + }, + { + name: 'CSTG option is null', + cstgOptions: '' + }, + { + name: 'serverPublicKey is not a string', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: {} } + }, + { + name: 'serverPublicKey not match regular expression', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: 'serverPublicKey' } + }, + { + name: 'subscriptionId is not a string', + cstgOptions: { subscriptionId: {}, serverPublicKey: cstgConfigParams.serverPublicKey } + }, + { + name: 'subscriptionId is empty', + cstgOptions: { subscriptionId: '', serverPublicKey: cstgConfigParams.serverPublicKey } + }, + ] + invalidConfigs.forEach(function(scenario) { + describe(`When ${scenario.name}`, function() { + it('should not generate token using identity', async () => { + config.setConfig(makePrebidConfig({ ...scenario.cstgOptions, email: 'test@email.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }); + }); + }); + }); + describe('When email is provided in different format', function () { + const testCases = [ + { originalEmail: 'TEst.TEST@Test.com ', normalizedEmail: 'test.test@test.com' }, + { originalEmail: 'test+test@test.com', normalizedEmail: 'test+test@test.com' }, + { originalEmail: ' testtest@test.com ', normalizedEmail: 'testtest@test.com' }, + { originalEmail: 'TEst.TEst+123@GMail.Com', normalizedEmail: 'testtest@gmail.com' } + ]; + testCases.forEach((testCase) => { + describe('it should normalize the email and generate token on normalized email', async () => { + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: testCase.originalEmail })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, { email: testCase.normalizedEmail }); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + } + + describe('When neither token nor CSTG config provided', function () { + describe('when there is a non-cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + describe('when there is a cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + it('the auction should has no uid2', async function () { + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) }); diff --git a/test/spec/modules/underdogmediaBidAdapter_spec.js b/test/spec/modules/underdogmediaBidAdapter_spec.js index 2d7c1f11178..c0e2e8dddce 100644 --- a/test/spec/modules/underdogmediaBidAdapter_spec.js +++ b/test/spec/modules/underdogmediaBidAdapter_spec.js @@ -5,6 +5,7 @@ import { spec, resetUserSync } from 'modules/underdogmediaBidAdapter.js'; +import { config } from '../../../src/config'; describe('UnderdogMedia adapter', function () { let bidRequests; @@ -763,6 +764,20 @@ describe('UnderdogMedia adapter', function () { expect(request.data.ref).to.equal(undefined); }); + it('should have pbTimeout to be 3001 if bidder timeout does not exists', function () { + config.setConfig({ bidderTimeout: '' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(3001) + }) + + it('should have pbTimeout to be a numerical value if bidder timeout is in a string', function () { + config.setConfig({ bidderTimeout: '1000' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(1000) + }) + it('should have pubcid if it exists', function () { let bidRequests = [{ adUnitCode: 'div-gpt-ad-1460505748561-0', diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 7f9c2e7b3d5..5cf53c661a9 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -39,12 +39,19 @@ const videoBidReq = [{ maxDuration: 30 } }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480], - placement: 1, - plcmt: 1 - }}, + ortb2Imp: { + ext: { + gpid: '/1111/gpid#728x90', + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 1, + plcmt: 1 + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -56,10 +63,19 @@ const videoBidReq = [{ placementId: '10433395', publisherId: 12345 }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480] - }}, + ortb2Imp: { + ext: { + data: { + pbadslot: '/1111/pbadslot#728x90' + } + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -463,12 +479,14 @@ describe('Undertone Adapter', () => { expect(bidVideo.video.skippable).to.equal(true); expect(bidVideo.video.placement).to.equal(1); expect(bidVideo.video.plcmt).to.equal(1); + expect(bidVideo.gpid).to.equal('/1111/gpid#728x90'); expect(bidVideo2.video.skippable).to.equal(null); expect(bidVideo2.video.maxDuration).to.equal(null); expect(bidVideo2.video.playbackMethod).to.equal(null); expect(bidVideo2.video.placement).to.equal(null); expect(bidVideo2.video.plcmt).to.equal(null); + expect(bidVideo2.gpid).to.equal('/1111/pbadslot#728x90'); }); it('should send all userIds data to server', function () { const request = spec.buildRequests(bidReqUserIds, bidderReq); diff --git a/test/spec/modules/unicornBidAdapter_spec.js b/test/spec/modules/unicornBidAdapter_spec.js index 0abb09bfb78..bd9175dac1e 100644 --- a/test/spec/modules/unicornBidAdapter_spec.js +++ b/test/spec/modules/unicornBidAdapter_spec.js @@ -1,4 +1,5 @@ import {assert, expect} from 'chai'; +import * as utils from 'src/utils.js'; import {spec} from 'modules/unicornBidAdapter.js'; import * as _ from 'lodash'; @@ -496,6 +497,17 @@ describe('unicornBidAdapterTest', () => { }); describe('buildBidRequest', () => { + const removeUntestableAttrs = data => { + delete data['device']; + delete data['site']['domain']; + delete data['site']['page']; + delete data['id']; + data['imp'].forEach(imp => { + delete imp['id']; + }) + delete data['user']['id']; + return data; + }; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { unicorn: { @@ -508,17 +520,6 @@ describe('unicornBidAdapterTest', () => { }); it('buildBidRequest', () => { const req = spec.buildRequests(validBidRequests, bidderRequest); - const removeUntestableAttrs = data => { - delete data['device']; - delete data['site']['domain']; - delete data['site']['page']; - delete data['id']; - data['imp'].forEach(imp => { - delete imp['id']; - }) - delete data['user']['id']; - return data; - }; const uid = JSON.parse(req.data)['user']['id']; const reqData = removeUntestableAttrs(JSON.parse(req.data)); const openRTBRequestData = removeUntestableAttrs(openRTBRequest); @@ -527,6 +528,28 @@ describe('unicornBidAdapterTest', () => { const uid2 = JSON.parse(req2.data)['user']['id']; assert.deepStrictEqual(uid, uid2); }); + it('test if contains ID5', () => { + let _validBidRequests = utils.deepClone(validBidRequests); + _validBidRequests[0].userId = { + id5id: { + uid: 'id5_XXXXX' + } + } + const req = spec.buildRequests(_validBidRequests, bidderRequest); + const reqData = removeUntestableAttrs(JSON.parse(req.data)); + const openRTBRequestData = removeUntestableAttrs(utils.deepClone(openRTBRequest)); + openRTBRequestData.user.eids = [ + { + source: 'id5-sync.com', + uids: [ + { + id: 'id5_XXXXX' + } + ] + } + ] + assert.deepStrictEqual(reqData, openRTBRequestData); + }) }); describe('interpretResponse', () => { diff --git a/test/spec/modules/unrulyBidAdapter_spec.js b/test/spec/modules/unrulyBidAdapter_spec.js index 6d1d8f9949f..abf1a54787d 100644 --- a/test/spec/modules/unrulyBidAdapter_spec.js +++ b/test/spec/modules/unrulyBidAdapter_spec.js @@ -42,9 +42,40 @@ describe('UnrulyAdapter', function () { } } - const createExchangeResponse = (...bids) => ({ - body: {bids} - }); + function createOutStreamExchangeAuctionConfig() { + return { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }; + + function createExchangeResponse (bidList, auctionConfigs = null) { + let bids = []; + if (Array.isArray(bidList)) { + bids = bidList; + } else if (bidList) { + bids.push(bidList); + } + + if (!auctionConfigs) { + return { + 'body': {bids} + }; + } + + return { + 'body': { + bids, + auctionConfigs + } + } + }; const inStreamServerResponse = { 'requestId': '262594d5d1f8104', @@ -486,7 +517,8 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b' } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; @@ -560,7 +592,8 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b', } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; @@ -651,13 +684,235 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b', } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); expect(result[0].data).to.deep.equal(expectedResult); }); + describe('Protected Audience Support', function() { + it('should return an array with 2 items and enabled protected audience', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': true, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + }, + { + 'bidder': 'unruly', + 'params': { + 'siteId': 2234554, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(2); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[1].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + expect(result[1].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + }); + it('should return an array with 2 items and enabled protected audience on only one unit', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': true, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + }, + { + 'bidder': 'unruly', + 'params': { + 'siteId': 2234554, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': {} + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(2); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[1].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + expect(result[1].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.be.undefined; + }); + it('disables configured protected audience when fledge is not availble', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': false, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(1); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.be.undefined; + }); + }); }); describe('interpretResponse', function () { @@ -705,7 +960,167 @@ describe('UnrulyAdapter', function () { renderer: fakeRenderer, mediaType: 'video' } - ]) + ]); + }); + + it('should return object with an array of bids and an array of auction configs when it receives a successful response from server', function () { + let bidId = '27a3ee1626a5c7' + const mockExchangeBid = createOutStreamExchangeBid({adUnitCode: 'video1', requestId: 'mockBidId'}); + const mockExchangeAuctionConfig = {}; + mockExchangeAuctionConfig[bidId] = createOutStreamExchangeAuctionConfig(); + const mockServerResponse = createExchangeResponse(mockExchangeBid, mockExchangeAuctionConfig); + const originalRequest = { + 'data': { + 'bidderRequest': { + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 480 + ], + [ + 300, + 250 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'bidId': bidId, + 'bidderRequestId': '12e00d17dff07b', + } + ] + } + } + }; + + expect(adapter.interpretResponse(mockServerResponse, originalRequest)).to.deep.equal({ + 'bids': [ + { + 'ext': { + 'statusCode': 1, + 'renderer': { + 'id': 'unruly_inarticle', + 'config': { + 'siteId': 123456, + 'targetingUUID': 'xxx-yyy-zzz' + }, + 'url': 'https://video.unrulymedia.com/native/prebid-loader.js' + }, + 'adUnitCode': 'video1' + }, + requestId: 'mockBidId', + bidderCode: 'unruly', + cpm: 20, + width: 323, + height: 323, + vastUrl: 'https://targeting.unrulymedia.com/in_article?uuid=74544e00-d43b-4f3a-a799-69d22ce979ce&supported_mime_type=application/javascript&supported_mime_type=video/mp4&tj=%7B%22site%22%3A%7B%22lang%22%3A%22en-GB%22%2C%22ref%22%3A%22%22%2C%22page%22%3A%22https%3A%2F%2Fdemo.unrulymedia.com%2FinArticle%2Finarticle_nypost_upbeat%2Ftravel_magazines.html%22%2C%22domain%22%3A%22demo.unrulymedia.com%22%7D%2C%22user%22%3A%7B%22profile%22%3A%7B%22quantcast%22%3A%7B%22segments%22%3A%5B%7B%22id%22%3A%22D%22%7D%2C%7B%22id%22%3A%22T%22%7D%5D%7D%7D%7D%7D&video_width=618&video_height=347', + netRevenue: true, + creativeId: 'mockBidId', + ttl: 360, + 'meta': { + 'mediaType': 'video', + 'videoContext': 'outstream' + }, + currency: 'USD', + renderer: fakeRenderer, + mediaType: 'video' + } + ], + 'fledgeAuctionConfigs': [{ + 'bidId': bidId, + 'config': { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }] + }); + }); + + it('should return object with an array of auction configs when it receives a successful response from server without bids', function () { + let bidId = '27a3ee1626a5c7'; + const mockExchangeAuctionConfig = {}; + mockExchangeAuctionConfig[bidId] = createOutStreamExchangeAuctionConfig(); + const mockServerResponse = createExchangeResponse(null, mockExchangeAuctionConfig); + const originalRequest = { + 'data': { + 'bidderRequest': { + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 480 + ], + [ + 300, + 250 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'bidId': bidId, + 'bidderRequestId': '12e00d17dff07b' + } + ] + } + } + }; + + expect(adapter.interpretResponse(mockServerResponse, originalRequest)).to.deep.equal({ + 'bids': [], + 'fledgeAuctionConfigs': [{ + 'bidId': bidId, + 'config': { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }] + }); }); it('should initialize and set the renderer', function () { @@ -875,7 +1290,7 @@ describe('UnrulyAdapter', function () { it('should return correct response for multiple bids', function () { const outStreamServerResponse = createOutStreamExchangeBid({adUnitCode: 'video1', requestId: 'mockBidId'}); - const mockServerResponse = createExchangeResponse(outStreamServerResponse, inStreamServerResponse, bannerServerResponse); + const mockServerResponse = createExchangeResponse([outStreamServerResponse, inStreamServerResponse, bannerServerResponse]); const expectedOutStreamResponse = outStreamServerResponse; expectedOutStreamResponse.mediaType = 'video'; @@ -890,7 +1305,7 @@ describe('UnrulyAdapter', function () { it('should return only valid bids', function () { const {ad, ...bannerServerResponseNoAd} = bannerServerResponse; - const mockServerResponse = createExchangeResponse(bannerServerResponseNoAd, inStreamServerResponse); + const mockServerResponse = createExchangeResponse([bannerServerResponseNoAd, inStreamServerResponse]); const expectedInStreamResponse = inStreamServerResponse; expectedInStreamResponse.mediaType = 'video'; diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index acc016a903d..1e909d79ed4 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2,7 +2,7 @@ import { attachIdSystem, auctionDelay, coreStorage, dep, - findRootDomain, + findRootDomain, getConsentHash, init, PBJS_USER_ID_OPTOUT_NAME, requestBidsHook, @@ -12,6 +12,7 @@ import { setSubmoduleRegistry, syncDelay, } from 'modules/userId/index.js'; +import {UID1_EIDS} from 'libraries/uid1Eids/uid1Eids.js'; import {createEidsArray, EID_CONFIG} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; @@ -21,7 +22,6 @@ import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; import {resetConsentData, } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; -import {find} from 'src/polyfill.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; @@ -56,7 +56,7 @@ import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; -import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {allConsent, GDPR_GVLIDS, gdprDataHandler} from '../../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../../src/activities/activities.js'; import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; @@ -100,6 +100,25 @@ describe('User ID', function () { } } + function createMockEid(name, source) { + return { + [name]: { + source: source || `${name}Source`, + atype: 3, + getValue: function(data) { + if (data.id) { + return data.id; + } else { + return data; + } + }, + getUidExt: function(data) { + return data.ext + } + } + } + } + function getAdUnitMock(code = 'adUnit-code') { return { code, @@ -380,11 +399,96 @@ describe('User ID', function () { expect(bid).to.not.have.deep.nested.property('userIdAsEids'); }); }); - // setCookie is called once in order to store consentData - expect(coreStorage.setCookie.callCount).to.equal(1); }); }); + describe('createEidsArray', () => { + beforeEach(() => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1', null, null, + {'mockId1': {source: 'mock1source', atype: 1}}), + createMockIdSubmodule('mockId2v1', null, null, + {'mockId2v1': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 1})}}), + createMockIdSubmodule('mockId2v2', null, null, + {'mockId2v2': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 2})}}), + ]); + }); + + it('should group UIDs by source and ext', () => { + const eids = createEidsArray({ + mockId1: ['mock-1-1', 'mock-1-2'], + mockId2v1: ['mock-2-1', 'mock-2-2'], + mockId2v2: ['mock-2-1', 'mock-2-2'] + }); + expect(eids).to.eql([ + { + source: 'mock1source', + uids: [ + { + id: 'mock-1-1', + atype: 1, + }, + { + id: 'mock-1-2', + atype: 1, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 1 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 2 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + } + ]) + }); + + it('when merging with pubCommonId, should not alter its eids', () => { + const uid = { + pubProvidedId: [ + { + source: 'mock1Source', + uids: [ + {id: 'uid2'} + ] + } + ], + mockId1: 'uid1', + }; + const eids = createEidsArray(uid); + expect(eids).to.have.length(1); + expect(eids[0].uids.map(u => u.id)).to.have.members(['uid1', 'uid2']); + expect(uid.pubProvidedId[0].uids).to.eql([{id: 'uid2'}]); + }); + }) + it('pbjs.getUserIds', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); @@ -560,10 +664,10 @@ describe('User ID', function () { it('pbjs.getUserIdsAsEids should prioritize user ids according to config available to core', () => { init(config); setSubmoduleRegistry([ - createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), - createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), - createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}), - createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}) + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}, null, createMockEid('uid2')), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}, null, createMockEid('pubcid')), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}, null, {...createMockEid('uid2'), ...createMockEid('merkleId'), ...createMockEid('lipb')}), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}, null, createMockEid('merkleId')) ]); config.setConfig({ userSync: { @@ -595,6 +699,38 @@ describe('User ID', function () { }); }); + it('pbjs.getUserIdsAsEids should prioritize the uid1 according to config available to core', () => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {tdid: {id: 'uid1_value'}}}, null, UID1_EIDS), + createMockIdSubmodule('mockId2Module', {id: {tdid: {id: 'uid1Id_value_from_mockId2Module'}}}, null, UID1_EIDS), + createMockIdSubmodule('mockId3Module', {id: {tdid: {id: 'uid1Id_value_from_mockId3Module'}}}, null, UID1_EIDS) + ]); + config.setConfig({ + userSync: { + idPriority: { + tdid: ['mockId2Module', 'mockId3Module', 'mockId1Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' } + ] + } + }); + + const ids = { + 'tdid': { id: 'uid1Id_value_from_mockId2Module' }, + }; + + return getGlobal().getUserIdsAsync().then(() => { + const eids = getGlobal().getUserIdsAsEids(); + const expected = createEidsArray(ids); + expect(eids).to.deep.equal(expected); + }) + }); + describe('EID updateConfig', () => { function mockSubmod(name, eids) { return createMockIdSubmodule(name, null, null, eids); @@ -929,6 +1065,7 @@ describe('User ID', function () { beforeEach(() => { mockIdCallback = sinon.stub(); + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); let mockIdSystem = { name: 'mockId', decode: function(value) { @@ -1155,7 +1292,7 @@ describe('User ID', function () { it('pbjs.refreshUserIds refreshes single', function() { coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('REFRESH', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('refreshedid', '', EXPIRED_COOKIE_DATE); let sandbox = sinon.createSandbox(); let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); @@ -3005,7 +3142,7 @@ describe('User ID', function () { expect(server.requests).to.be.empty; return endAuction(); }).then(() => { - expect(server.requests[0].url).to.equal('/any/unifiedid/url'); + expect(server.requests[0].url).to.match(/\/any\/unifiedid\/url/); }); }); @@ -3119,17 +3256,12 @@ describe('User ID', function () { } }; - consentData = { - gdprApplies: true, - consentString: 'mockString', - apiVersion: 1, - hasValidated: true // mock presence of GPDR enforcement module - } // clear cookies expStr = (new Date(Date.now() + 25000).toUTCString()); coreStorage.setCookie(mockIdCookieName, '', EXPIRED_COOKIE_DATE); coreStorage.setCookie(`${mockIdCookieName}_last`, '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(`${mockIdCookieName}_cst`, '', EXPIRED_COOKIE_DATE); + allConsent.reset(); // init adUnits = [getAdUnitMock()]; @@ -3143,10 +3275,20 @@ describe('User ID', function () { config.resetConfig(); }); - it('calls getId if no stored consent data and refresh is not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + function setStorage({ + val = JSON.stringify({id: '1234'}), + lastDelta = 60 * 1000, + cst = null + } = {}) { + coreStorage.setCookie(mockIdCookieName, val, expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - lastDelta).toUTCString()), expStr); + if (cst != null) { + coreStorage.setCookie(`${mockIdCookieName}_cst`, cst, expStr); + } + } + it('calls getId if no stored consent data and refresh is not needed', function () { + setStorage({lastDelta: 1000}); config.setConfig(userIdConfig); let innerAdUnits; @@ -3160,9 +3302,7 @@ 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}_last`, (new Date(Date.now() - 60 * 1000).toUTCString()), expStr); - + setStorage(); config.setConfig(userIdConfig); let innerAdUnits; @@ -3176,11 +3316,7 @@ 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}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData(); - + setStorage({cst: ''}); config.setConfig(userIdConfig); let innerAdUnits; @@ -3194,10 +3330,10 @@ 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}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData({...consentData, consentString: 'different'}); + setStorage({cst: getConsentHash()}); + gdprDataHandler.setConsentData({ + consentString: 'different' + }); config.setConfig(userIdConfig); @@ -3212,10 +3348,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}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData({...consentData}); + setStorage({lastDelta: 1000, cst: getConsentHash()}); config.setConfig(userIdConfig); @@ -3473,11 +3606,16 @@ describe('User ID', function () { it('pbjs.getUserIdsAsEidBySource with priority config available to core', () => { init(config); + const uid2Eids = createMockEid('uid2', 'uidapi.com') + const pubcEids = createMockEid('pubcid', 'pubcid.org') + const liveIntentEids = createMockEid('lipb', 'liveintent.com') + const merkleEids = createMockEid('merkleId', 'merkleinc.com') + setSubmoduleRegistry([ - createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), - createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), - createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}), - createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}) + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}, null, uid2Eids), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}, null, {...pubcEids, ...liveIntentEids}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}, null, {...uid2Eids, ...pubcEids, ...liveIntentEids}), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}, null, merkleEids) ]); config.setConfig({ userSync: { diff --git a/test/spec/modules/viantOrtbBidAdapter_spec.js b/test/spec/modules/viantOrtbBidAdapter_spec.js new file mode 100644 index 00000000000..73fdb7f3dc8 --- /dev/null +++ b/test/spec/modules/viantOrtbBidAdapter_spec.js @@ -0,0 +1,475 @@ +import { spec, converter } from 'modules/viantOrtbBidAdapter.js'; +import {assert, expect} from 'chai'; +import { deepClone } from '../../../src/utils'; +import {buildWindowTree} from '../../helpers/refererDetectionHelper'; +import {detectReferer} from '../../../src/refererDetection'; +describe('viantOrtbBidAdapter', function () { + function testBuildRequests(bidRequests, bidderRequestBase) { + let clonedBidderRequest = deepClone(bidderRequestBase); + clonedBidderRequest.bids = bidRequests; + let requests = spec.buildRequests(bidRequests, clonedBidderRequest); + return requests + } + describe('isBidRequestValid', function() { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': 'some-PlacementId_1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [728, 90] + ] + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + describe('core', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when publisherId not passed', function () { + let bid = makeBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if placementId is not passed ', function () { + let bid = makeBid(); + delete bid.params.placementId; + bid.ortb2Imp = { + + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if mediaTypes.banner is Not passed', function () { + let bid = makeBid(); + delete bid.mediaTypes + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('banner', function () { + it('should return true if banner.pos is passed correctly', function () { + let bid = makeBid(); + bid.mediaTypes.banner.pos = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('video', function () { + describe('and request config uses mediaTypes', () => { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 30 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, makeBid()); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + }); + + describe('native', function () { + describe('and request config uses mediaTypes', () => { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 30 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let nativeBidWithMediaTypes = Object.assign({}, makeBid()); + nativeBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(nativeBidWithMediaTypes)).to.equal(false); + }); + }); + }); + }); + + describe('buildRequests-banner', function () { + const baseBannerBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'gdprConsent': { + 'consentString': 'consentString', + 'gdprApplies': true, + }, + 'uspConsent': '1YYY', + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('test regs', function () { + const gdprBaseBidderRequest = Object.assign({}, baseBidderRequest, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + }, + uspConsent: '1YYN' + }); + const request = testBuildRequests(baseBannerBidRequests, gdprBaseBidderRequest)[0]; + expect(request.data.regs.ext).to.have.property('gdpr', 1); + expect(request.data.regs.ext).to.have.property('us_privacy', '1YYN'); + }); + + it('sends bid request to our endpoint that makes sense', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.be.not.empty; + expect(request.data).to.be.not.null; + }); + it('sends bid requests to the correct endpoint', function () { + const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].url; + expect(url).to.equal('https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder'); + }); + + it('sends site', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].data; + expect(requestBody.site).to.be.not.null; + }); + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(728); + expect(requestBody.imp[0].banner.format[0].h).to.equal(90); + }); + + it('sets the banner pos correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].mediaTypes.banner.pos = 1; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].banner.pos).to.equal(1); + }); + }); + + if (FEATURES.VIDEO) { + describe('buildRequests-video', function () { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 31 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + + it('assert video and its fields is present in imp ', function () { + let requests = spec.buildRequests([makeBid()], {referrerInfo: {}}); + let clonedRequests = deepClone(requests) + assert.equal(clonedRequests[0].data.imp[0].video.mimes[0], 'video/mp4') + assert.equal(clonedRequests[0].data.imp[0].video.maxduration, 31) + assert.equal(clonedRequests[0].data.imp[0].video.placement, 1) + assert.equal(clonedRequests[0].method, 'POST') + }); + }); + } + + describe('interpretResponse', function () { + const baseBannerBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('empty bid response test', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + let bidResponse = {nbr: 0}; // Unknown error + let bids = spec.interpretResponse({body: bidResponse}, request); + expect(bids.length).to.equal(0); + }); + + it('bid response is a banner', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + let bidResponse = { + seatbid: [{ + bid: [{ + impid: '243310435309b5', + price: 2, + w: 728, + h: 90, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'USD' + }; + let bids = spec.interpretResponse({body: bidResponse}, request); + expect(bids.length).to.equal(1); + let bid = bids[0]; + it('should return the proper mediaType', function () { + it('should return a creativeId', function () { + expect(bid.mediaType).to.equal('banner'); + }); + }); + it('should return a price', function () { + expect(bid.cpm).to.equal(bidResponse.seatbid[0].bid[0].price); + }); + + it('should return a request id', function () { + expect(bid.requestId).to.equal(bidResponse.seatbid[0].bid[0].impid); + }); + + it('should return width and height for the creative', function () { + expect(bid.width).to.equal(bidResponse.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidResponse.seatbid[0].bid[0].h); + }); + it('should return a creativeId', function () { + expect(bid.creativeId).to.equal(bidResponse.seatbid[0].bid[0].crid); + }); + it('should return an ad', function () { + expect(bid.ad).to.equal(bidResponse.seatbid[0].bid[0].adm); + }); + + it('should return a deal id if it exists', function () { + expect(bid.dealId).to.equal(bidResponse.seatbid[0].bid[0].dealid); + }); + + it('should have a time-to-live of 5 minutes', function () { + expect(bid.ttl).to.equal(300); + }); + + it('should always return net revenue', function () { + expect(bid.netRevenue).to.equal(true); + }); + it('should return a currency', function () { + expect(bid.currency).to.equal(bidResponse.cur); + }); + }); + }); + describe('interpretResponse-Video', function () { + const baseVideoBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 31 + } + }, + 'sizes': [[640, 480]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('bid response is a video', function () { + const request = testBuildRequests(baseVideoBidRequests, baseBidderRequest)[0]; + const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '243310435309b5', + 'price': 1.09, + 'adid': '144762342', + 'nurl': 'http://0.0.0.0:8181/nurl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 480, + 'w': 640 + } + ] + } + ], + 'cur': 'USD' + }; + let bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request); + expect(bids.length).to.equal(1); + let bid = bids[0]; + it('should return the proper mediaType', function () { + expect(bid.mediaType).to.equal('video'); + }); + it('should return correct Ad Markup', function () { + expect(bid.vastXml).to.equal(''); + }); + it('should return correct Notification', function () { + expect(bid.vastUrl).to.equal('http://0.0.0.0:8181/nurl'); + }); + it('should return correct Cpm', function () { + expect(bid.cpm).to.equal(1.09); + }); + }); + }); +}); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 864f2b8551c..5515002a054 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -19,6 +19,7 @@ import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; import {BANNER, VIDEO} from '../../../src/mediaTypes'; import {config} from '../../../src/config'; +import {deepSetValue} from 'src/utils.js'; export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; @@ -108,7 +109,19 @@ const BIDDER_REQUEST = { 'ortb2': { 'site': { 'cat': ['IAB2'], - 'pagecat': ['IAB2-2'] + 'pagecat': ['IAB2-2'], + 'content': { + 'data': [{ + 'name': 'example.com', + 'ext': { + 'segtax': 7 + }, + 'segments': [ + {'id': 'segId1'}, + {'id': 'segId2'} + ] + }] + } }, 'regs': { 'gpp': 'gpp_string', @@ -131,6 +144,15 @@ const BIDDER_REQUEST = { 'bitness': '64', 'architecture': '' } + }, + user: { + data: [ + { + ext: {segtax: 600, segclass: '1'}, + name: 'example.com', + segment: [{id: '243'}], + }, + ], } }, }; @@ -318,6 +340,23 @@ describe('VidazooBidAdapter', function () { 'bitness': '64', 'architecture': '' }, + contentData: [{ + 'name': 'example.com', + 'ext': { + 'segtax': 7 + }, + 'segments': [ + {'id': 'segId1'}, + {'id': 'segId2'} + ] + }], + userData: [ + { + ext: {segtax: 600, segclass: '1'}, + name: 'example.com', + segment: [{id: '243'}], + }, + ], uniqueDealId: `${hashUrl}_${Date.now().toString()}`, uqs: getTopWindowQueryParams(), isStorageAllowed: true, @@ -340,7 +379,8 @@ describe('VidazooBidAdapter', function () { } } } - }); + }) + ; }); it('should build banner request for each size', function () { @@ -405,6 +445,23 @@ describe('VidazooBidAdapter', function () { gpid: '1234567890', cat: ['IAB2'], pagecat: ['IAB2-2'], + contentData: [{ + 'name': 'example.com', + 'ext': { + 'segtax': 7 + }, + 'segments': [ + {'id': 'segId1'}, + {'id': 'segId2'} + ] + }], + userData: [ + { + ext: {segtax: 600, segclass: '1'}, + name: 'example.com', + segment: [{id: '243'}], + }, + ], webSessionId: webSessionId } }); @@ -478,6 +535,23 @@ describe('VidazooBidAdapter', function () { gpid: '1234567890', cat: ['IAB2'], pagecat: ['IAB2-2'], + contentData: [{ + 'name': 'example.com', + 'ext': { + 'segtax': 7 + }, + 'segments': [ + {'id': 'segId1'}, + {'id': 'segId2'} + ] + }], + userData: [ + { + ext: {segtax: 600, segclass: '1'}, + name: 'example.com', + segment: [{id: '243'}], + }, + ], webSessionId: webSessionId }; @@ -523,6 +597,15 @@ describe('VidazooBidAdapter', function () { expect(requests).to.have.length(2); }); + it('should set fledge correctly if enabled', function () { + config.resetConfig(); + const bidderRequest = utils.deepClone(BIDDER_REQUEST); + bidderRequest.fledgeEnabled = true; + deepSetValue(bidderRequest, 'ortb2Imp.ext.ae', 1); + const requests = adapter.buildRequests([BID], bidderRequest); + expect(requests[0].data.fledge).to.equal(1); + }); + after(function () { $$PREBID_GLOBAL$$.bidderSettings = {}; config.resetConfig(); @@ -648,6 +731,14 @@ describe('VidazooBidAdapter', function () { expect(responses).to.have.length(1); expect(responses[0].ttl).to.equal(300); }); + + it('should add nurl if exists on response', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].nurl = 'https://test.com/win-notice?test=123'; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].nurl).to.equal('https://test.com/win-notice?test=123'); + }); }); describe('user id system', function () { @@ -833,4 +924,66 @@ describe('VidazooBidAdapter', function () { expect(parsed).to.be.equal(value); }); }); + + describe('validate onBidWon', function () { + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function () { + utils.triggerPixel.restore(); + }); + + it('should call triggerPixel if nurl exists', function () { + const bid = { + adUnitCode: 'div-gpt-ad-12345-0', + adId: '2d52001cabd527', + auctionId: '1fdb5ff1b6eaa7', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + status: 'rendered', + timeToRespond: 100, + cpm: 0.8, + originalCpm: 0.8, + creativeId: '12610997325162499419', + currency: 'USD', + originalCurrency: 'USD', + height: 250, + mediaType: 'banner', + nurl: 'https://test.com/win-notice?test=123', + netRevenue: true, + requestId: '2d52001cabd527', + ttl: 30, + width: 300 + }; + adapter.onBidWon(bid); + expect(utils.triggerPixel.called).to.be.true; + + const url = utils.triggerPixel.args[0]; + + expect(url[0]).to.be.equal('https://test.com/win-notice?test=123&adId=2d52001cabd527&creativeId=12610997325162499419&auctionId=1fdb5ff1b6eaa7&transactionId=c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf&adUnitCode=div-gpt-ad-12345-0&cpm=0.8¤cy=USD&originalCpm=0.8&originalCurrency=USD&netRevenue=true&mediaType=banner&timeToRespond=100&status=rendered'); + }); + + it('should not call triggerPixel if nurl does not exist', function () { + const bid = { + adUnitCode: 'div-gpt-ad-12345-0', + adId: '2d52001cabd527', + auctionId: '1fdb5ff1b6eaa7', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + status: 'rendered', + timeToRespond: 100, + cpm: 0.8, + originalCpm: 0.8, + creativeId: '12610997325162499419', + currency: 'USD', + originalCurrency: 'USD', + height: 250, + mediaType: 'banner', + netRevenue: true, + requestId: '2d52001cabd527', + ttl: 30, + width: 300 + }; + adapter.onBidWon(bid); + expect(utils.triggerPixel.called).to.be.false; + }); + }); }); diff --git a/test/spec/modules/videoModule/pbVideo_spec.js b/test/spec/modules/videoModule/pbVideo_spec.js index 2e26737da40..1ccd9766eab 100644 --- a/test/spec/modules/videoModule/pbVideo_spec.js +++ b/test/spec/modules/videoModule/pbVideo_spec.js @@ -1,3 +1,4 @@ +import 'src/prebid.js'; import { expect } from 'chai'; import { PbVideo } from 'modules/videoModule'; import CONSTANTS from 'src/constants.json'; @@ -26,7 +27,8 @@ function resetTestVars() { onEvents: sinon.spy(), getOrtbVideo: () => ortbVideoMock, getOrtbContent: () => ortbContentMock, - setAdTagUrl: sinon.spy() + setAdTagUrl: sinon.spy(), + hasProviderFor: sinon.spy(), }; getConfigMock = () => {}; requestBidsMock = { diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 9a486cd6c34..5528705efd7 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/visxBidAdapter.js'; +import { spec, storage } from 'modules/visxBidAdapter.js'; import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -82,6 +82,9 @@ describe('VisxAdapter', function () { }); return res; } + + let cookiesAreEnabledStub, localStorageIsEnabledStub; + const bidderRequest = { timeout: 3000, refererInfo: { @@ -180,6 +183,24 @@ describe('VisxAdapter', function () { 'ext': {'bidder': {'uid': 903537}} }]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should attach valid params to the tag', function () { const firstBid = bidRequests[0]; const bids = [firstBid]; @@ -422,6 +443,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -449,6 +471,24 @@ describe('VisxAdapter', function () { } ]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should send requst for banner bid', function () { const request = spec.buildRequests([bidRequests[0]], bidderRequest); const payload = parseRequest(request.url); @@ -486,6 +526,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -529,10 +570,23 @@ describe('VisxAdapter', function () { documentStub.withArgs('visx-adunit-element-2').returns({ id: 'visx-adunit-element-2' }); + + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); }); after(function() { sandbox.restore(); + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; }); it('should find ad slot by ad unit code as element id', function () { @@ -1203,6 +1257,7 @@ describe('VisxAdapter', function () { const request = spec.buildRequests(bidRequests); const pendingUrl = 'https://t.visx.net/track/pending/123123123'; const winUrl = 'https://t.visx.net/track/win/53245341'; + const runtimeUrl = 'https://t.visx.net/track/status/12345678'; const expectedResponse = [ { 'requestId': '300bfeb0d71a5b', @@ -1227,7 +1282,8 @@ describe('VisxAdapter', function () { 'ext': { 'events': { 'pending': pendingUrl, - 'win': winUrl + 'win': winUrl, + 'runtime': runtimeUrl }, 'targeting': { 'hb_visx_product': 'understitial', @@ -1244,6 +1300,9 @@ describe('VisxAdapter', function () { pending: pendingUrl, win: winUrl, }); + utils.deepSetValue(serverResponse.bid[0], 'ext.visx.events', { + runtime: runtimeUrl + }); const result = spec.interpretResponse({'body': {'seatbid': [serverResponse]}}, request); expect(result).to.deep.equal(expectedResponse); }); @@ -1271,6 +1330,39 @@ describe('VisxAdapter', function () { expect(utils.triggerPixel.calledOnceWith(trackUrl)).to.equal(true); }); + it('onBidWon with runtime tracker (0 < timeToRespond <= 5000 )', function () { + const trackUrl = 'https://t.visx.net/track/win/123123123'; + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '1', ext: { events: { win: trackUrl, runtime: runtimeUrl } }, timeToRespond: 100 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledTwice).to.equal(true); + expect(utils.triggerPixel.calledWith(trackUrl)).to.equal(true); + expect(utils.triggerPixel.calledWith(runtimeUrl.replace('{STATUS_CODE}', 999002))).to.equal(true); + }); + + it('onBidWon with runtime tracker (timeToRespond <= 0 )', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '2', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 0 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999000))).to.equal(true); + }); + + it('onBidWon with runtime tracker (timeToRespond > 5000 )', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '3', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 5001 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999100))).to.equal(true); + }); + + it('onBidWon runtime tracker should be called once per auction', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid1 = { auctionId: '4', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 100 }; + spec.onBidWon(bid1); + const bid2 = { auctionId: '4', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 200 }; + spec.onBidWon(bid2); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999002))).to.equal(true); + }); + it('onTimeout', function () { const data = [{ timeout: 3000, adUnitCode: 'adunit-code-1', auctionId: '1cbd2feafe5e8b', bidder: 'visx', bidId: '23423', params: [{ uid: '1' }] }]; const expectedData = [{ timeout: 3000, params: [{ uid: 1 }] }]; @@ -1323,4 +1415,100 @@ describe('VisxAdapter', function () { expect(query).to.deep.equal({}); }); }); + + describe('first party user id', function () { + const USER_ID_KEY = '__vads'; + const USER_ID_DUMMY_VALUE_COOKIE = 'dummy_id_cookie'; + const USER_ID_DUMMY_VALUE_LOCAL_STORAGE = 'dummy_id_local_storage'; + + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + const bidderRequest = { + timeout: 3000, + refererInfo: { + page: 'https://example.com' + } + }; + + beforeEach(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: true + } + }; + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + }); + + afterEach(() => { + cookiesAreEnabledStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub && getCookieStub.restore(); + getDataFromLocalStorageStub && getDataFromLocalStorageStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + + it('should not pass user id if both cookies and local storage are not available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user).to.be.undefined; + }); + + it('should get user id from cookie if available', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + getCookieStub = sinon.stub(storage, 'getCookie'); + getCookieStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_COOKIE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_COOKIE); + }); + + it('should get user id from local storage if available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + }); + + it('should create user id and store it in cookies (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getCookie(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + + it('should create user id and store it in local storage (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getDataFromLocalStorage(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + }); }); diff --git a/test/spec/modules/vrtcalBidAdapter_spec.js b/test/spec/modules/vrtcalBidAdapter_spec.js index 609d5d0a5f7..938934170e9 100644 --- a/test/spec/modules/vrtcalBidAdapter_spec.js +++ b/test/spec/modules/vrtcalBidAdapter_spec.js @@ -29,7 +29,12 @@ describe('vrtcalBidAdapter', function () { 'bidderRequestId': 'br0001', 'auctionId': 'auction0001', 'userIdAsEids': {}, - timeout: 435 + timeout: 435, + + refererInfo: { + page: 'page' + } + } ]; @@ -129,4 +134,39 @@ describe('vrtcalBidAdapter', function () { ).to.be.true }) }) + + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://usync.vrtcal.com/i?ssp=1804&synctype=iframe'; + const syncurl_redirect = 'https://usync.vrtcal.com/i?ssp=1804&synctype=redirect'; + + it('base iframe sync pper config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('base redirect sync per config', function() { + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: syncurl_redirect + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with ccpa data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, 'ccpa_consent_string', undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=ccpa_consent_string&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gdpr data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: 1, consentString: 'gdpr_consent_string'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=1&gdpr_consent=gdpr_consent_string&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gpp data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined, {gppString: 'gpp_consent_string', applicableSections: [1, 5]})).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=gpp_consent_string&gpp_sid=1,5&surl=' + }]); + }); + }) }) diff --git a/test/spec/modules/weboramaRtdProvider_spec.js b/test/spec/modules/weboramaRtdProvider_spec.js index 7de8474d7c9..d562d9ffd13 100644 --- a/test/spec/modules/weboramaRtdProvider_spec.js +++ b/test/spec/modules/weboramaRtdProvider_spec.js @@ -48,6 +48,120 @@ describe('weboramaRtdProvider', function() { }; expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); }); + + it('instantiate with empty sfbxLiteData should return true', function() { + const moduleConfig = { + params: { + sfbxLiteDataConf: {}, + } + }; + expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); + }); + + describe('webo user data should check gdpr consent', function() { + it('should initialize if gdpr does not applies', function() { + const moduleConfig = { + params: { + weboUserDataConf: {} + } + }; + const userConsent = { + gdpr: { + gdprApplies: false, + }, + } + expect(weboramaSubmodule.init(moduleConfig, userConsent)).to.equal(true); + }); + it('should initialize if gdpr applies and consent is ok', function() { + const moduleConfig = { + params: { + weboUserDataConf: {} + } + }; + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + 5: true, + 6: true, + 9: true, + }, + }, + specialFeatureOptins: { + 1: true, + }, + vendor: { + consents: { + 284: true, + }, + } + }, + }, + } + expect(weboramaSubmodule.init(moduleConfig, userConsent)).to.equal(true); + }); + it('should NOT initialize if gdpr applies and consent is nok: miss consent vendor id', function() { + const moduleConfig = { + params: { + weboUserDataConf: {} + } + }; + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { + 1: true, + 3: true, + 4: true, + }, + }, + specialFeatureOptins: {}, + vendor: { + consents: { + 284: false, + }, + } + }, + }, + } + expect(weboramaSubmodule.init(moduleConfig, userConsent)).to.equal(false); + }); + it('should NOT initialize if gdpr applies and consent is nok: miss one purpose id', function() { + const moduleConfig = { + params: { + weboUserDataConf: {} + } + }; + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { + 1: false, + 3: true, + 4: true, + }, + }, + specialFeatureOptins: {}, + vendor: { + consents: { + 284: true, + }, + } + }, + }, + } + expect(weboramaSubmodule.init(moduleConfig, userConsent)).to.equal(false); + }); + }); }); describe('Handle Set Targeting and Bid Request', function() { diff --git a/test/spec/modules/yandexAnalyticsAdapter_spec.js b/test/spec/modules/yandexAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..ca9b29d13a5 --- /dev/null +++ b/test/spec/modules/yandexAnalyticsAdapter_spec.js @@ -0,0 +1,147 @@ +import * as sinon from 'sinon'; +import yandexAnalytics, { EVENTS_TO_TRACK } from 'modules/yandexAnalyticsAdapter.js'; +import * as log from '../../../src/utils.js' +import * as events from '../../../src/events.js'; + +describe('Yandex analytics adapter testing', () => { + const sandbox = sinon.createSandbox(); + let clock; + let logError; + let getEvents; + let onEvent; + const counterId = 123; + const counterWindowKey = 'yaCounter123'; + + beforeEach(() => { + yandexAnalytics.counters = {}; + yandexAnalytics.counterInitTimeouts = {}; + yandexAnalytics.bufferedEvents = []; + yandexAnalytics.oneCounterInited = false; + clock = sinon.useFakeTimers(); + logError = sandbox.stub(log, 'logError'); + sandbox.stub(log, 'logInfo'); + getEvents = sandbox.stub(events, 'getEvents').returns([]); + onEvent = sandbox.stub(events, 'on'); + sandbox.stub(window.document, 'createElement').callsFake((tag) => { + const element = { + tag, + events: {}, + attributes: {}, + addEventListener: (event, cb) => { + element.events[event] = cb; + }, + removeEventListener: (event, cb) => { + chai.expect(element.events[event]).to.equal(cb); + }, + setAttribute: (attr, val) => { + element.attributes[attr] = val; + }, + }; + return element; + }); + }); + + afterEach(() => { + window.Ya = null; + window[counterWindowKey] = null; + sandbox.restore(); + clock.restore(); + }); + + it('fails if timeout for counter insertion is exceeded', () => { + yandexAnalytics.enableAnalytics({ + options: { + counters: [ + 123, + ], + }, + }); + clock.tick(25001); + chai.expect(yandexAnalytics.bufferedEvents).to.deep.equal([]); + sinon.assert.calledWith(logError, `Can't find metrika counter after 25 seconds.`); + sinon.assert.calledWith(logError, `Aborting yandex analytics provider initialization.`); + }); + + it('fails if no valid counters provided', () => { + yandexAnalytics.enableAnalytics({ + options: { + counters: [ + 'abc', + ], + }, + }); + sinon.assert.calledWith(logError, 'options.counters contains no valid counter ids'); + }); + + it('subscribes to events if counter is already present', () => { + window[counterWindowKey] = { + pbjs: sandbox.stub(), + }; + + getEvents.returns([ + { + eventType: EVENTS_TO_TRACK[0], + }, + { + eventType: 'Some_untracked_event', + } + ]); + const eventsToSend = [{ + event: EVENTS_TO_TRACK[0], + data: { + eventType: EVENTS_TO_TRACK[0], + } + }]; + + yandexAnalytics.enableAnalytics({ + options: { + counters: [ + counterId, + ], + }, + }); + + EVENTS_TO_TRACK.forEach((eventName, i) => { + const [event, callback] = onEvent.getCall(i).args; + chai.expect(event).to.equal(eventName); + callback(i); + eventsToSend.push({ + event: eventName, + data: i, + }); + }); + + clock.tick(1501); + + const [ sentEvents ] = window[counterWindowKey].pbjs.getCall(0).args; + chai.expect(sentEvents).to.deep.equal(eventsToSend); + }); + + it('waits for counter initialization', () => { + window.Ya = {}; + // Simulatin metrika script initialization + yandexAnalytics.enableAnalytics({ + options: { + counters: [ + counterId, + ], + }, + }); + + // Sending event + const [event, eventCallback] = onEvent.getCall(0).args; + eventCallback({}); + + const counterPbjsMethod = sandbox.stub(); + window[`yaCounter${counterId}`] = { + pbjs: counterPbjsMethod, + }; + clock.tick(2001); + + const [ sentEvents ] = counterPbjsMethod.getCall(0).args; + chai.expect(sentEvents).to.deep.equal([{ + event, + data: {}, + }]); + }); +}); diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index eef476e21d2..140be4121ec 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -1,8 +1,8 @@ import { assert, expect } from 'chai'; -import { spec, NATIVE_ASSETS } from 'modules/yandexBidAdapter.js'; -import { parseUrl } from 'src/utils.js'; -import { BANNER, NATIVE } from '../../../src/mediaTypes'; +import { NATIVE_ASSETS, spec } from 'modules/yandexBidAdapter.js'; +import * as utils from 'src/utils.js'; import { config } from '../../../src/config'; +import { BANNER, NATIVE } from '../../../src/mediaTypes'; describe('Yandex adapter', function () { describe('isBidRequestValid', function () { @@ -41,11 +41,45 @@ describe('Yandex adapter', function () { }); describe('buildRequests', function () { + /** @type {import('../../../src/auction').BidderRequest} */ const bidderRequest = { - refererInfo: { - domain: 'ya.ru', - ref: 'https://ya.ru/', - page: 'https://ya.ru/', + ortb2: { + site: { + domain: 'ya.ru', + ref: 'https://ya.ru/', + page: 'https://ya.ru/', + publisher: { + domain: 'ya.ru', + }, + }, + device: { + w: 1600, + h: 900, + dnt: 0, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + language: 'en', + sua: { + source: 1, + platform: { + brand: 'macOS', + }, + browsers: [ + { + brand: 'Not_A Brand', + version: ['8'], + }, + { + brand: 'Chromium', + version: ['120'], + }, + { + brand: 'Google Chrome', + version: ['120'], + }, + ], + mobile: 0, + }, + }, }, gdprConsent: { gdprApplies: 1, @@ -71,7 +105,7 @@ describe('Yandex adapter', function () { expect(method).to.equal('POST'); - const parsedRequestUrl = parseUrl(url); + const parsedRequestUrl = utils.parseUrl(url); const { search: query } = parsedRequestUrl expect(parsedRequestUrl.hostname).to.equal('bs.yandex.ru'); @@ -100,12 +134,70 @@ describe('Yandex adapter', function () { const bannerRequest = getBidRequest(); const requests = spec.buildRequests([bannerRequest], bidderRequest); const { url } = requests[0]; - const parsedRequestUrl = parseUrl(url); + const parsedRequestUrl = utils.parseUrl(url); const { search: query } = parsedRequestUrl expect(query['ssp-cur']).to.equal('USD'); }); + it('should send eids and ortb2 user data if defined', function() { + const bidderRequestWithUserData = { + ...bidderRequest, + ortb2: { + ...bidderRequest.ortb2, + user: { + data: [ + { + ext: { segtax: 600, segclass: '1' }, + name: 'example.com', + segment: [{ id: '243' }], + }, + { + ext: { segtax: 600, segclass: '1' }, + name: 'ads.example.org', + segment: [{ id: '243' }], + }, + ], + }, + } + }; + const bidRequestExtra = { + userIdAsEids: [{ + source: 'sharedid.org', + uids: [{ id: '01', atype: 1 }], + }], + }; + + const expected = { + ext: { + eids: bidRequestExtra.userIdAsEids, + }, + data: bidderRequestWithUserData.ortb2.user.data, + }; + + const bannerRequest = getBidRequest(bidRequestExtra); + const requests = spec.buildRequests([bannerRequest], bidderRequestWithUserData); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request.data).to.exist; + const { data } = request; + + expect(data.user).to.exist; + expect(data.user).to.deep.equal(expected); + }); + + it('should send site', function() { + const expected = { + site: bidderRequest.ortb2.site + }; + + const requests = spec.buildRequests([getBidRequest()], bidderRequest); + + expect(requests[0].data.site).to.deep.equal(expected.site); + }); + describe('banner', () => { it('should create valid banner object', () => { const bannerRequest = getBidRequest({ @@ -443,6 +535,60 @@ describe('Yandex adapter', function () { }); }); }); + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if bid does not contain nurl', function() { + spec.onBidWon({}); + + expect(utils.triggerPixel.callCount).to.equal(0) + }) + + it('Should trigger pixel if bid has nurl', function() { + spec.onBidWon({ + nurl: 'https://example.com/some-tracker', + timeToRespond: 378, + }); + + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker?rtt=378') + }) + + it('Should trigger pixel if bid has nurl with path & params', function() { + spec.onBidWon({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2', + timeToRespond: 378, + }); + + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&rtt=378') + }) + + it('Should trigger pixel if bid has nurl with path & params and rtt macros', function() { + spec.onBidWon({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=${RTT}', + timeToRespond: 378, + }); + + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=378') + }) + + it('Should trigger pixel if bid has nurl and there is no timeToRespond param, but has rtt macros in nurl', function() { + spec.onBidWon({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=${RTT}', + }); + + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=-1') + }) + }) }); function getBidConfig() { diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 93c231c816b..751dff4fe33 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -226,6 +226,36 @@ const PVID_RESPONSE = Object.assign({}, VIDEO_RESPONSE, { pvid: '43513f11-55a0-4a83-94e5-0ebc08f54a2c', }); +const DIGITAL_SERVICES_ACT_RESPONSE = Object.assign({}, RESPONSE, { + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + } +}); + +const DIGITAL_SERVICES_ACT_CONFIG = { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + }, + } + }, + } +} + const REQPARAMS = { json: true, ts: 1234567890, @@ -486,6 +516,75 @@ describe('yieldlabBidAdapter', () => { expect(request.url).to.not.include('sizes'); }); }); + + describe('Digital Services Act handling', () => { + beforeEach(() => { + config.setConfig(DIGITAL_SERVICES_ACT_CONFIG); + }); + + afterEach(() => { + config.resetConfig(); + }); + + it('does pass dsarequired parameter', () => { + let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG }); + expect(request.url).to.include('dsarequired=1'); + }); + + it('does pass dsapubrender parameter', () => { + let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG }); + expect(request.url).to.include('dsapubrender=2'); + }); + + it('does pass dsadatatopub parameter', () => { + let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG }); + expect(request.url).to.include('dsadatatopub=3'); + }); + + it('does pass dsadomain parameter', () => { + let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG }); + expect(request.url).to.include('dsadomain=test.com'); + }); + + it('does pass encoded dsaparams parameter', () => { + let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG }); + expect(request.url).to.include('dsaparams=1%2C2%2C3'); + }); + + it('does pass multiple transparencies in dsatransparency param', () => { + const DSA_CONFIG_WITH_MULTIPLE_TRANSPARENCIES = { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [ + { + domain: 'test.com', + dsaparams: [1, 2, 3] + }, + { + domain: 'example.com', + dsaparams: [4, 5, 6] + } + ] + } + } + } + } + }; + + config.setConfig(DSA_CONFIG_WITH_MULTIPLE_TRANSPARENCIES); + + let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DSA_CONFIG_WITH_MULTIPLE_TRANSPARENCIES }); + + expect(request.url).to.include('dsatransparency=test.com~1_2_3~~example.com~4_5_6'); + expect(request.url).to.not.include('dsadomain'); + expect(request.url).to.not.include('dsaparams'); + }); + }); }); describe('interpretResponse', () => { @@ -676,6 +775,17 @@ describe('yieldlabBidAdapter', () => { const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST()], queryParams: REQPARAMS_IAB_CONTENT}); expect(result[0].vastUrl).to.include('&iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0'); }); + + it('should get digital services act object in matched bid response', () => { + const result = spec.interpretResponse({body: [DIGITAL_SERVICES_ACT_RESPONSE]}, {validBidRequests: [{...DEFAULT_REQUEST(), ...DIGITAL_SERVICES_ACT_CONFIG}], queryParams: REQPARAMS}); + + expect(result[0].requestId).to.equal('2d925f27f5079f'); + expect(result[0].meta.dsa.behalf).to.equal('some-behalf'); + expect(result[0].meta.dsa.paid).to.equal('some-paid'); + expect(result[0].meta.dsa.transparency[0].domain).to.equal('test.com'); + expect(result[0].meta.dsa.transparency[0].dsaparams).to.deep.equal([1, 2, 3]); + expect(result[0].meta.dsa.adrender).to.equal(1); + }); }); describe('getUserSyncs', () => { diff --git a/test/spec/modules/yieldloveBidAdapter_spec.js b/test/spec/modules/yieldloveBidAdapter_spec.js new file mode 100644 index 00000000000..b142eef0ffa --- /dev/null +++ b/test/spec/modules/yieldloveBidAdapter_spec.js @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { spec } from 'modules/yieldloveBidAdapter.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +// test params +const pid = 34437; +const rid = 'website.com'; + +describe('Yieldlove Bid Adaper', function () { + const bidRequests = [ + { + 'bidder': 'yieldlove', + 'adUnitCode': 'adunit-code', + 'sizes': [ [300, 250] ], + 'params': { + pid, + rid + } + } + ]; + + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: 'aaaa', + price: 0.5, + w: 300, + h: 250, + adm: '
test
', + crid: '1234', + } + ] + } + ], + ext: {} + } + } + + describe('isBidRequestValid', () => { + const bid = bidRequests[0]; + + 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 present', function () { + const invalidBid = { ...bid, params: {} }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "pid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, pid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "rid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, rid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build the request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = request.data; + const url = request.url; + + expect(url).to.equal(ENDPOINT_URL); + + expect(payload.site).to.exist; + expect(payload.site.publisher).to.exist; + expect(payload.site.publisher.id).to.exist; + expect(payload.site.publisher.id).to.equal(rid); + expect(payload.site.domain).to.exist; + expect(payload.site.domain).to.equal(rid); + + expect(payload.imp).to.exist; + expect(payload.imp[0]).to.exist; + expect(payload.imp[0].ext).to.exist; + expect(payload.imp[0].ext.prebid).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(pid.toString()); + }); + }); + + describe('interpretResponse', () => { + it('should interpret the response by pushing it in the bids elem', function () { + const allResponses = spec.interpretResponse(serverResponse); + const response = allResponses[0]; + const seatbid = serverResponse.body.seatbid[0].bid[0]; + + expect(response.requestId).to.exist; + expect(response.requestId).to.equal(seatbid.impid); + expect(response.cpm).to.exist; + expect(response.cpm).to.equal(seatbid.price); + expect(response.width).to.exist; + expect(response.width).to.equal(seatbid.w); + expect(response.height).to.exist; + expect(response.height).to.equal(seatbid.h); + expect(response.ad).to.exist; + expect(response.ad).to.equal(seatbid.adm); + expect(response.ttl).to.exist; + expect(response.creativeId).to.exist; + expect(response.creativeId).to.equal(seatbid.crid); + expect(response.netRevenue).to.exist; + expect(response.currency).to.exist; + }); + }); + + describe('getUserSyncs', function() { + it('should retrieve user iframe syncs', function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], undefined, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=NaN&gdpr_consent=&' + }]); + + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], { gdprApplies: true, consentString: 'example' }, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=1&gdpr_consent=example&' + }]); + }); + }); +}) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 3706f770da8..f37ef9178dd 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -47,7 +47,7 @@ describe('YieldmoAdapter', function () { video: { playerSize: [640, 480], context: 'instream', - mimes: ['video/mp4'] + mimes: ['video/mp4'], }, }, params: { @@ -61,11 +61,11 @@ describe('YieldmoAdapter', function () { api: [2, 3], skipppable: true, playbackmethod: [1, 2], - ...videoParams - } + ...videoParams, + }, }, transactionId: '54a58774-7a41-494e-8cbc-fa7b79164f0c', - ...rootParams + ...rootParams, }); const mockBidderRequest = (params = {}, bids = [mockBannerBid()]) => ({ @@ -74,7 +74,6 @@ describe('YieldmoAdapter', function () { bidderRequestId: '14c4ede8c693f', bids, auctionStart: 1520001292880, - timeout: 3000, start: 1520001292884, doneCbCallCount: 0, refererInfo: { @@ -169,6 +168,14 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); }); + it('should pass default timeout in bid request', function () { + const requests = build([mockBannerBid()]); + expect(requests[0].data.tmax).to.equal(400); + }); + it('should pass tmax to bid request', function () { + const requests = build([mockBannerBid()], mockBidderRequest({timeout: 1000})); + expect(requests[0].data.tmax).to.equal(1000); + }); it('should not blow up if crumbs is undefined', function () { expect(function () { build([mockBannerBid({crumbs: undefined})]); @@ -387,6 +394,61 @@ describe('YieldmoAdapter', function () { expect(placementInfo).to.include('"gpid":"/6355419/Travel/Europe/France/Paris"'); }); + it('should add topics to the banner bid request', function () { + const biddata = build([mockBannerBid()], mockBidderRequest({ortb2: { user: { + data: [ + { + ext: { + segtax: 600, + segclass: '2206021246', + }, + segment: ['7', '8', '9'], + }, + ], + }}})); + + expect(biddata[0].data.topics).to.equal(JSON.stringify({ + taxonomy: 600, + classifier: '2206021246', + topics: [7, 8, 9], + })); + }); + + it('should add cdep to the banner bid request', function () { + const biddata = build( + [mockBannerBid()], + mockBidderRequest({ + ortb2: { + device: { + ext: { + cdep: 'test_cdep' + }, + }, + }, + }) + ); + + expect(biddata[0].data.cdep).to.equal( + 'test_cdep' + ); + }); + + it('should send gpc in the banner bid request', function () { + const biddata = build( + [mockBannerBid()], + mockBidderRequest({ + ortb2: { + regs: { + ext: { + gpc: '1' + }, + }, + }, + }) + ); + expect(biddata[0].data.gpc).to.equal('1'); + }); + it('should add eids to the banner bid request', function () { const params = { userIdAsEids: [{ @@ -425,6 +487,18 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); }); + it('should not require params.video if required props in mediaTypes.video', function () { + videoBid.mediaTypes.video = { + ...videoBid.mediaTypes.video, + ...videoBid.params.video + }; + delete videoBid.params.video; + const requests = build([videoBid]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); + it('should add mediaTypes.video prop to the imp.video prop', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['minduration'] = 40; expect(buildVideoBidAndGetVideoParam().minduration).to.equal(40); @@ -441,6 +515,16 @@ describe('YieldmoAdapter', function () { expect(buildVideoBidAndGetVideoParam().minduration).to.deep.equal(['video/mp4']); }); + it('should add plcmt value to the imp.video', function () { + const videoBid = mockVideoBid({}, {}, { plcmt: 1 }); + expect(utils.deepAccess(videoBid, 'params.video')['plcmt']).to.equal(1); + }); + + it('should add start delay if plcmt value is not 1', function () { + const videoBid = mockVideoBid({}, {}, { plcmt: 2 }); + expect(build([videoBid])[0].data.imp[0].video.startdelay).to.equal(0); + }); + it('should override mediaTypes.video.mimes prop if params.video.mimes is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['mimes'] = ['video/mp4']; utils.deepAccess(videoBid, 'params.video')['mimes'] = ['video/mkv']; @@ -577,6 +661,51 @@ describe('YieldmoAdapter', function () { }; expect(buildAndGetData([mockVideoBid({...params})]).user.eids).to.eql(params.fakeUserIdAsEids); }); + + it('should add topics to the bid request', function () { + let videoBidder = mockBidderRequest( + { + ortb2: { + user: { + data: [ + { + ext: { + segtax: 600, + segclass: '2206021246', + }, + segment: ['7', '8', '9'], + }, + ], + }, + }, + }, + [mockVideoBid()] + ); + let payload = buildAndGetData([mockVideoBid()], 0, videoBidder); + expect(payload.topics).to.deep.equal({ + taxonomy: 600, + classifier: '2206021246', + topics: [7, 8, 9], + }); + }); + + it('should send gpc in the bid request', function () { + let videoBidder = mockBidderRequest( + { + ortb2: { + regs: { + ext: { + gpc: '1', + }, + }, + }, + }, + [mockVideoBid()] + ); + let payload = buildAndGetData([mockVideoBid()], 0, videoBidder); + expect(payload.regs.ext.gpc).to.equal('1'); + }); + it('should add device info to payload if available', function () { let videoBidder = mockBidderRequest({ ortb2: { device: { diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index 15a1155f378..54b61f19506 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -1,20 +1,21 @@ import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; import {config} from 'src/config'; import CONSTANTS from 'src/constants.json'; +import {server} from '../../mocks/xhr.js'; import {logError} from '../../../src/utils'; let utils = require('src/utils'); let events = require('src/events'); -const MOCK = { - STUB: { - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' - }, +const EVENTS = { AUCTION_END: { - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'timestamp': 1638441234544, 'auctionEnd': 1638441234784, 'auctionStatus': 'completed', + 'metrics': { + 'someMetric': 1 + }, 'adUnits': [ { 'code': '/19968336/header-bid-tag-0', @@ -60,7 +61,7 @@ const MOCK = { 600 ] ], - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83' + 'transactionId': '6b29369c' } ], 'adUnitCodes': [ @@ -69,18 +70,11 @@ const MOCK = { 'bidderRequests': [ { 'bidderCode': 'zeta_global_ssp', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'bidderRequestId': '1207cb49191887', 'bids': [ { 'bidder': 'zeta_global_ssp', - 'params': { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } - }, 'mediaTypes': { 'banner': { 'sizes': [ @@ -96,7 +90,7 @@ const MOCK = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -109,7 +103,7 @@ const MOCK = { ], 'bidId': '206be9a13236af', 'bidderRequestId': '1207cb49191887', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -132,7 +126,7 @@ const MOCK = { }, { 'bidderCode': 'appnexus', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'bidderRequestId': '32b97f0a935422', 'bids': [ { @@ -155,7 +149,7 @@ const MOCK = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -168,7 +162,7 @@ const MOCK = { ], 'bidId': '41badc0e164c758', 'bidderRequestId': '32b97f0a935422', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -211,7 +205,7 @@ const MOCK = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -224,7 +218,7 @@ const MOCK = { ], 'bidId': '41badc0e164c758', 'bidderRequestId': '32b97f0a935422', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -249,12 +243,12 @@ const MOCK = { 'netRevenue': true, 'meta': { 'advertiserDomains': [ - 'viaplay.fi' + 'example.adomain' ] }, 'originalCpm': 2.258302852806723, 'originalCurrency': 'USD', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'responseTimestamp': 1638441234670, 'requestTimestamp': 1638441234547, 'bidder': 'zeta_global_ssp', @@ -274,7 +268,7 @@ const MOCK = { 'hb_size': '480x320', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'viaplay.fi' + 'hb_adomain': 'example.adomain' } } ], @@ -309,17 +303,20 @@ const MOCK = { 'cpm': 2.258302852806723, 'currency': 'USD', 'ad': 'test_ad', + 'metrics': { + 'someMetric': 0 + }, 'ttl': 200, 'creativeId': '456456456', 'netRevenue': true, 'meta': { 'advertiserDomains': [ - 'viaplay.fi' + 'example.adomain' ] }, 'originalCpm': 2.258302852806723, 'originalCurrency': 'USD', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'responseTimestamp': 1638441234670, 'requestTimestamp': 1638441234547, 'bidder': 'zeta_global_ssp', @@ -339,16 +336,12 @@ const MOCK = { 'hb_size': '480x320', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'viaplay.fi' + 'hb_adomain': 'example.adomain' }, 'status': 'rendered', 'params': [ { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } + 'nonZetaParam': 'nonZetaValue' } ] }, @@ -358,14 +351,11 @@ const MOCK = { describe('Zeta Global SSP Analytics Adapter', function() { let sandbox; - let xhr; let requests; beforeEach(function() { sandbox = sinon.sandbox.create(); - requests = []; - xhr = sandbox.useFakeXMLHttpRequest(); - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); }); @@ -395,33 +385,48 @@ describe('Zeta Global SSP Analytics Adapter', function() { zetaAnalyticsAdapter.disableAnalytics(); }); - it('events are sent', function() { - this.timeout(5000); - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_END, MOCK.AUCTION_END); - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.NO_BID, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_WON, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_DONE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.SET_TARGETING, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.ADD_AD_UNITS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_FAILED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED); - events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.STALE_RENDER, MOCK.STUB); + it('Move ZetaParams through analytics events', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); + + expect(requests.length).to.equal(2); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionSucceeded.bid.params[0]).to.be.deep.equal(EVENTS.AUCTION_END.adUnits[0].bids[0].params); + expect(EVENTS.AUCTION_END.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + }); + + it('Keep only needed fields', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); expect(requests.length).to.equal(2); - expect(JSON.parse(requests[0].requestBody)).to.deep.equal(MOCK.AUCTION_END); - expect(JSON.parse(requests[1].requestBody)).to.deep.equal(MOCK.AD_RENDER_SUCCEEDED); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionEnd.adUnitCodes).to.be.undefined; + expect(auctionEnd.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.auctionEnd).to.be.undefined; + expect(auctionEnd.auctionId).to.be.equal('75e394d9'); + expect(auctionEnd.bidderRequests[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.bidderRequests[0].bids[0].bidId).to.be.equal('206be9a13236af'); + expect(auctionEnd.bidderRequests[0].bids[0].adUnitCode).to.be.equal('/19968336/header-bid-tag-0'); + expect(auctionEnd.bidsReceived[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.bidsReceived[0].adserverTargeting.hb_adomain).to.be.equal('example.adomain'); + expect(auctionEnd.bidsReceived[0].auctionId).to.be.equal('75e394d9'); + + expect(auctionSucceeded.adId).to.be.equal('5759bb3ef7be1e8'); + expect(auctionSucceeded.bid.auctionId).to.be.equal('75e394d9'); + expect(auctionSucceeded.bid.requestId).to.be.equal('206be9a13236af'); + expect(auctionSucceeded.bid.bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionSucceeded.bid.creativeId).to.be.equal('456456456'); + expect(auctionSucceeded.bid.size).to.be.equal('480x320'); + expect(auctionSucceeded.doc.location.hostname).to.be.equal('localhost'); }); }); }); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 601f4546a29..f9cfe2dde6a 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -1,5 +1,6 @@ import {spec} from '../../../modules/zeta_global_sspBidAdapter.js' import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {deepClone} from '../../../src/utils'; describe('Zeta Ssp Bid Adapter', function () { const eids = [ @@ -48,12 +49,17 @@ describe('Zeta Ssp Bid Adapter', function () { }, tags: { someTag: 444, + emptyTag: {}, + nullTag: null, + complexEmptyTag: { + empty: {}, + nullValue: null + } }, sid: 'publisherId', - shortname: 'test_shortname', tagid: 'test_tag_id', site: { - page: 'testPage' + page: 'http://www.zetaglobal.com/page?param=value' }, app: { bundle: 'testBundle' @@ -124,7 +130,34 @@ describe('Zeta Ssp Bid Adapter', function () { uspConsent: 'someCCPAString', params: params, userIdAsEids: eids, - timeout: 500 + timeout: 500, + ortb2: { + device: { + sua: { + mobile: 1, + architecture: 'arm', + platform: { + brand: 'Chrome', + version: ['102'] + } + } + }, + user: { + data: [ + { + ext: { + segtax: 600, + segclass: 'classifier_v1' + }, + segment: [ + { id: '3' }, + { id: '44' }, + { id: '59' } + ] + } + ] + } + } }]; const bannerWithFewSizesRequest = [{ @@ -176,6 +209,7 @@ describe('Zeta Ssp Bid Adapter', function () { id: '12345', seatbid: [ { + seat: '1', bid: [ { id: 'auctionId', @@ -201,7 +235,7 @@ describe('Zeta Ssp Bid Adapter', function () { id: '123', site: { id: 'SITE_ID', - page: 'page.com', + page: 'http://www.zetaglobal.com/page?param=value', domain: 'domain.com' }, user: { @@ -229,7 +263,7 @@ describe('Zeta Ssp Bid Adapter', function () { id: '123', site: { id: 'SITE_ID', - page: 'page.com', + page: 'http://www.zetaglobal.com/page?param=value', domain: 'domain.com' }, user: { @@ -253,11 +287,13 @@ describe('Zeta Ssp Bid Adapter', function () { }; it('Test the bid validation function', function () { - const validBid = spec.isBidRequestValid(bannerRequest[0]); - const invalidBid = spec.isBidRequestValid(null); + const invalidBid = deepClone(bannerRequest[0]); + invalidBid.params = {}; + const isValidBid = spec.isBidRequestValid(bannerRequest[0]); + const isInvalidBid = spec.isBidRequestValid(null); - expect(validBid).to.be.true; - expect(invalidBid).to.be.false; + expect(isValidBid).to.be.true; + expect(isInvalidBid).to.be.false; }); it('Test provide eids', function () { @@ -276,7 +312,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test page and domain in site', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); - expect(payload.site.page).to.eql('http://www.zetaglobal.com/page?param=value'); + expect(payload.site.page).to.eql('zetaglobal.com/page'); expect(payload.site.domain).to.eql('zetaglobal.com'); }); @@ -453,7 +489,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in banner request', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -462,7 +498,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in video request', function () { const request = spec.buildRequests(videoRequest, videoRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -471,7 +507,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test multi imp', function () { const request = spec.buildRequests(multiImpRequest, multiImpRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.imp.length).to.eql(2); @@ -566,6 +602,7 @@ describe('Zeta Ssp Bid Adapter', function () { expect(bidResponse[0].mediaType).to.eql(BANNER); expect(bidResponse[0].ad).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); expect(bidResponse[0].vastXml).to.be.undefined; + expect(bidResponse[0].dspId).to.eql(zetaResponse.body.seatbid[0].seat); }); it('Test the response default mediaType:video', function () { @@ -575,6 +612,7 @@ describe('Zeta Ssp Bid Adapter', function () { expect(bidResponse[0].mediaType).to.eql(VIDEO); expect(bidResponse[0].ad).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); expect(bidResponse[0].vastXml).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); + expect(bidResponse[0].dspId).to.eql(zetaResponse.body.seatbid[0].seat); }); it('Test the response mediaType:video from ext param', function () { @@ -589,6 +627,7 @@ describe('Zeta Ssp Bid Adapter', function () { expect(bidResponse[0].mediaType).to.eql(VIDEO); expect(bidResponse[0].ad).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); expect(bidResponse[0].vastXml).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); + expect(bidResponse[0].dspId).to.eql(zetaResponse.body.seatbid[0].seat); }); it('Test the response mediaType:banner from ext param', function () { @@ -603,5 +642,41 @@ describe('Zeta Ssp Bid Adapter', function () { expect(bidResponse[0].mediaType).to.eql(BANNER); expect(bidResponse[0].ad).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); expect(bidResponse[0].vastXml).to.be.undefined; + expect(bidResponse[0].dspId).to.eql(zetaResponse.body.seatbid[0].seat); + }); + + it('Test provide segments into the request', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.user.data[0].segment.length).to.eql(3); + expect(payload.user.data[0].segment[0].id).to.eql('3'); + expect(payload.user.data[0].segment[1].id).to.eql('44'); + expect(payload.user.data[0].segment[2].id).to.eql('59'); + }); + + it('Test provide device params', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + + expect(payload.device.sua.mobile).to.eql(1); + expect(payload.device.sua.architecture).to.eql('arm'); + expect(payload.device.sua.platform.brand).to.eql('Chrome'); + expect(payload.device.sua.platform.version[0]).to.eql('102'); + + expect(payload.device.ua).to.not.be.undefined; + expect(payload.device.language).to.not.be.undefined; + expect(payload.device.w).to.not.be.undefined; + expect(payload.device.h).to.not.be.undefined; + }); + + it('Test that all empties are removed', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + + expect(payload.ext.tags.someTag).to.eql(444); + + expect(payload.ext.tags.emptyTag).to.be.undefined; + expect(payload.ext.tags.nullTag).to.be.undefined; + expect(payload.ext.tags.complexEmptyTag).to.be.undefined; }); }); diff --git a/test/spec/modules/zmaticooBidAdapter_spec.js b/test/spec/modules/zmaticooBidAdapter_spec.js new file mode 100644 index 00000000000..bb89984c738 --- /dev/null +++ b/test/spec/modules/zmaticooBidAdapter_spec.js @@ -0,0 +1,266 @@ +import {checkParamDataType, spec} from '../../../modules/zmaticooBidAdapter.js' +import utils, {deepClone} from '../../../src/utils'; +import {expect} from 'chai'; + +describe('zMaticoo Bidder Adapter', function () { + const bannerRequest = [{ + auctionId: '223', + mediaTypes: { + banner: { + sizes: [[320, 50]], + } + }, + refererInfo: { + page: 'testprebid.com' + }, + params: { + user: { + uid: '12345', + buyeruid: '12345' + }, + pubId: 'prebid-test', + test: 1, + bidfloor: 1, + tagid: 'test' + } + }]; + const bannerRequest1 = [{ + auctionId: '223', + mediaTypes: { + banner: { + sizes: [[320, 50]], + } + }, + refererInfo: { + page: 'testprebid.com' + }, + params: { + user: { + uid: '12345', + buyeruid: '12345' + }, + pubId: 'prebid-test', + test: 1, + tagid: 'test' + }, + gdprConsent: { + gdprApplies: 1, + consentString: 'consentString' + }, + getFloor: function () { + return { + currency: 'USD', + floor: 0.5, + } + }, + }]; + const videoRequest = [{ + auctionId: '223', + mediaTypes: { + video: { + playerSize: [480, 320], + mimes: ['video/mp4'], + context: 'instream', + placement: 1, + maxduration: 30, + minduration: 15, + pos: 1, + startdelay: 10, + protocols: [2, 3], + api: [2, 3], + playbackmethod: [2, 6], + skip: 10, + } + }, + refererInfo: { + page: 'testprebid.com' + }, + params: { + user: { + uid: '12345', + buyeruid: '12345' + }, + pubId: 'prebid-test', + test: 1, + tagid: 'test', + bidfloor: 1 + } + }]; + + const videoRequest1 = [{ + auctionId: '223', + mediaTypes: { + video: { + playerSize: [[480, 320]], + mimes: ['video/mp4'], + context: 'instream', + placement: 1, + maxduration: 30, + minduration: 15, + pos: 1, + startdelay: 10, + protocols: [2, 3], + api: [2, 3], + playbackmethod: [2, 6], + skip: 10, + } + }, + params: { + user: { + uid: '12345', + buyeruid: '12345' + }, + pubId: 'prebid-test', + test: 1, + tagid: 'test', + bidfloor: 1 + } + }]; + + describe('isBidRequestValid', function () { + it('this is valid bidrequest', function () { + const validBid = spec.isBidRequestValid(videoRequest[0]); + expect(validBid).to.be.true; + }); + it('missing required bid data {bid}', function () { + const invalidBid = spec.isBidRequestValid(null); + expect(invalidBid).to.be.false; + }); + it('missing required params.pubId', function () { + const request = deepClone(videoRequest[0]) + delete request.params.pubId + const invalidBid = spec.isBidRequestValid(request); + expect(invalidBid).to.be.false; + }); + }) + describe('buildRequests', function () { + it('Test the banner 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; + }); + it('Test the video request processing function', function () { + const request = spec.buildRequests(videoRequest, videoRequest[0]); + expect(request).to.not.be.empty; + const payload = request.data; + expect(payload).to.not.be.empty; + }); + it('Test the param', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.imp[0].tagid).to.eql(videoRequest[0].params.tagid); + expect(payload.imp[0].bidfloor).to.eql(videoRequest[0].params.bidfloor); + }); + it('Test video object', function () { + const request = spec.buildRequests(videoRequest, videoRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.exist; + expect(payload.imp[0].video.minduration).to.eql(videoRequest[0].mediaTypes.video.minduration); + expect(payload.imp[0].video.maxduration).to.eql(videoRequest[0].mediaTypes.video.maxduration); + expect(payload.imp[0].video.protocols).to.eql(videoRequest[0].mediaTypes.video.protocols); + expect(payload.imp[0].video.mimes).to.eql(videoRequest[0].mediaTypes.video.mimes); + expect(payload.imp[0].video.w).to.eql(480); + expect(payload.imp[0].video.h).to.eql(320); + expect(payload.imp[0].banner).to.be.undefined; + }); + + it('Test video isArray size', function () { + const request = spec.buildRequests(videoRequest1, videoRequest1[0]); + const payload = JSON.parse(request.data); + expect(payload.imp[0].video.w).to.eql(480); + expect(payload.imp[0].video.h).to.eql(320); + }); + it('Test banner object', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.be.undefined; + expect(payload.imp[0].banner).to.exist; + }); + + it('Test provide gdpr and ccpa values in payload', function () { + const request = spec.buildRequests(bannerRequest1, bannerRequest1[0]); + const payload = JSON.parse(request.data); + expect(payload.user.ext.consent).to.eql('consentString'); + expect(payload.regs.ext.gdpr).to.eql(1); + }); + + it('Test bidfloor is function', function () { + const request = spec.buildRequests(bannerRequest1, bannerRequest1[0]); + const payload = JSON.parse(request.data); + expect(payload.imp[0].bidfloor).to.eql(0.5); + }); + }); + describe('checkParamDataType tests', function () { + it('return the expected datatypes', function () { + assert.isString(checkParamDataType('Right string', 'test', 'string')); + assert.isBoolean(checkParamDataType('Right bool', true, 'boolean')); + assert.isNumber(checkParamDataType('Right number', 10, 'number')); + assert.isArray(checkParamDataType('Right array', [10, 11], 'array')); + }); + + it('return undefined var for wrong datatypes', function () { + expect(checkParamDataType('Wrong string', 10, 'string')).to.be.undefined; + expect(checkParamDataType('Wrong bool', 10, 'boolean')).to.be.undefined; + expect(checkParamDataType('Wrong number', 'one', 'number')).to.be.undefined; + expect(checkParamDataType('Wrong array', false, 'array')).to.be.undefined; + }); + }) + describe('interpretResponse', function () { + const responseBody = { + id: '12345', + seatbid: [ + { + bid: [ + { + id: 'auctionId', + impid: 'impId', + price: 0.0, + adm: 'adMarkup', + crid: 'creativeId', + adomain: ['test.com'], + h: 50, + w: 320, + nurl: 'https://gwbudgetali.iymedia.me/budget.php', + ext: { + vast_url: '', + prebid: { + type: 'banner' + } + } + } + ] + } + ], + cur: 'USD' + }; + it('Test the response parsing function', function () { + const receivedBid = responseBody.seatbid[0].bid[0]; + const response = {}; + response.body = responseBody; + const bidResponse = spec.interpretResponse(response, null); + expect(bidResponse).to.not.be.empty; + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal(receivedBid.adm); + expect(bid.cpm).to.equal(receivedBid.price); + expect(bid.height).to.equal(receivedBid.h); + expect(bid.width).to.equal(receivedBid.w); + expect(bid.requestId).to.equal(receivedBid.impid); + expect(bid.vastXml).to.equal(receivedBid.ext.vast_url); + expect(bid.meta.advertiserDomains).to.equal(receivedBid.adomain); + expect(bid.mediaType).to.equal(receivedBid.ext.prebid.type); + expect(bid.nurl).to.equal(receivedBid.nurl); + }); + }); + describe('onBidWon', function () { + it('should make an ajax call with the original cpm', function () { + const bid = { + nurl: 'http://test.com/win?auctionPrice=${AUCTION_PRICE}', + cpm: 2.1, + } + const bidWonResult = spec.onBidWon(bid) + expect(bidWonResult).to.equal(true) + }); + }) +}); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index dee177d4b9b..9184601a76d 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -9,7 +9,12 @@ import { decorateAdUnitsWithNativeParams, isOpenRTBBidRequestValid, isNativeOpenRTBBidValid, - toOrtbNativeRequest, toOrtbNativeResponse, legacyPropertiesToOrtbNative, fireImpressionTrackers, fireClickTrackers, + toOrtbNativeRequest, + toOrtbNativeResponse, + legacyPropertiesToOrtbNative, + fireImpressionTrackers, + fireClickTrackers, + setNativeResponseProperties, } from 'src/native.js'; import CONSTANTS from 'src/constants.json'; import { stubAuctionIndex } from '../helpers/indexStub.js'; @@ -19,7 +24,7 @@ const utils = require('src/utils'); const bid = { adId: '123', - transactionId: 'au', + adUnitId: 'au', native: { title: 'Native Creative', body: 'Cool description great stuff', @@ -49,7 +54,7 @@ const bid = { const ortbBid = { adId: '123', - transactionId: 'au', + adUnitId: 'au', native: { ortb: { assets: [ @@ -106,7 +111,7 @@ const ortbBid = { const completeNativeBid = { adId: '123', - transactionId: 'au', + adUnitId: 'au', native: { ...bid.native, ...ortbBid.native @@ -157,7 +162,7 @@ const ortbRequest = { } const bidWithUndefinedFields = { - transactionId: 'au', + adUnitId: 'au', native: { title: 'Native Creative', body: undefined, @@ -209,7 +214,7 @@ describe('native.js', function () { it('sends placeholders for configured assets', function () { const adUnit = { - transactionId: 'au', + adUnitId: 'au', nativeParams: { body: { sendId: true }, clickUrl: { sendId: true }, @@ -246,7 +251,7 @@ describe('native.js', function () { it('should only include native targeting keys with values', function () { const adUnit = { - transactionId: 'au', + adUnitId: 'au', nativeParams: { body: { sendId: true }, clickUrl: { sendId: true }, @@ -273,7 +278,7 @@ describe('native.js', function () { it('should only include targeting that has sendTargetingKeys set to true', function () { const adUnit = { - transactionId: 'au', + adUnitId: 'au', nativeParams: { image: { required: true, @@ -294,7 +299,7 @@ describe('native.js', function () { it('should only include targeting if sendTargetingKeys not set to false', function () { const adUnit = { - transactionId: 'au', + adUnitId: 'au', nativeParams: { image: { required: true, @@ -345,73 +350,10 @@ describe('native.js', function () { ]); }); - it('should copy over rendererUrl to bid object and include it in targeting', function () { - const adUnit = { - transactionId: 'au', - nativeParams: { - image: { - required: true, - sizes: [150, 50], - }, - title: { - required: true, - len: 80, - }, - rendererUrl: { - url: 'https://www.renderer.com/', - }, - }, - }; - const targeting = getNativeTargeting(bid, deps(adUnit)); - - expect(Object.keys(targeting)).to.deep.equal([ - CONSTANTS.NATIVE_KEYS.title, - CONSTANTS.NATIVE_KEYS.body, - CONSTANTS.NATIVE_KEYS.cta, - CONSTANTS.NATIVE_KEYS.image, - CONSTANTS.NATIVE_KEYS.icon, - CONSTANTS.NATIVE_KEYS.sponsoredBy, - CONSTANTS.NATIVE_KEYS.clickUrl, - CONSTANTS.NATIVE_KEYS.privacyLink, - CONSTANTS.NATIVE_KEYS.rendererUrl, - ]); - - expect(bid.native.rendererUrl).to.deep.equal('https://www.renderer.com/'); - delete bid.native.rendererUrl; - }); - - it('should copy over adTemplate to bid object and include it in targeting', function () { - const adUnit = { - transactionId: 'au', - nativeParams: { - image: { - required: true, - sizes: [150, 50], - }, - title: { - required: true, - len: 80, - }, - adTemplate: '

##hb_native_body##

', - }, - }; - const targeting = getNativeTargeting(bid, deps(adUnit)); - - expect(Object.keys(targeting)).to.deep.equal([ - CONSTANTS.NATIVE_KEYS.title, - CONSTANTS.NATIVE_KEYS.body, - CONSTANTS.NATIVE_KEYS.cta, - CONSTANTS.NATIVE_KEYS.image, - CONSTANTS.NATIVE_KEYS.icon, - CONSTANTS.NATIVE_KEYS.sponsoredBy, - CONSTANTS.NATIVE_KEYS.clickUrl, - CONSTANTS.NATIVE_KEYS.privacyLink, - ]); - - expect(bid.native.adTemplate).to.deep.equal( - '

##hb_native_body##

' - ); - delete bid.native.adTemplate; + it('should include rendererUrl in targeting', function () { + const rendererUrl = 'https://www.renderer.com/'; + const targeting = getNativeTargeting({...bid, native: {...bid.native, rendererUrl: {url: rendererUrl}}}, deps({})); + expect(targeting[CONSTANTS.NATIVE_KEYS.rendererUrl]).to.eql(rendererUrl); }); it('fires impression trackers', function () { @@ -631,7 +573,8 @@ describe('native.js', function () { eventtrackers: [ { event: 1, method: 1, url: 'https://sampleurl.com' }, { event: 1, method: 2, url: 'https://sampleurljs.com' } - ] + ], + imptrackers: [ 'https://sample-imp.com' ] } describe('toLegacyResponse', () => { it('returns assets in legacy format for ortb responses', () => { @@ -640,8 +583,61 @@ describe('native.js', function () { expect(actual.title).to.equal('vtitle'); expect(actual.clickUrl).to.equal('url'); expect(actual.javascriptTrackers).to.equal(''); - expect(actual.impressionTrackers.length).to.equal(1); - expect(actual.impressionTrackers[0]).to.equal('https://sampleurl.com'); + expect(actual.impressionTrackers.length).to.equal(2); + expect(actual.impressionTrackers).to.contain('https://sampleurl.com'); + expect(actual.impressionTrackers).to.contain('https://sample-imp.com'); + }); + }); + + describe('setNativeResponseProperties', () => { + let adUnit; + beforeEach(() => { + adUnit = { + mediaTypes: { + native: {}, + }, + nativeParams: {} + }; + }); + it('sets legacy response', () => { + adUnit.nativeOrtbRequest = { + assets: [{ + id: 1, + data: { + type: 2 + } + }] + }; + const ortbBid = { + ...bid, + native: { + ortb: { + link: { + url: 'clickurl' + }, + assets: [{ + id: 1, + data: { + value: 'body' + } + }] + } + } + }; + setNativeResponseProperties(ortbBid, adUnit); + expect(ortbBid.native.clickUrl).to.eql('clickurl'); + expect(ortbBid.native.body).to.eql('body'); + }); + + it('sets rendererUrl', () => { + adUnit.nativeParams.rendererUrl = {url: 'renderer'}; + setNativeResponseProperties(bid, adUnit); + expect(bid.native.rendererUrl).to.eql('renderer'); + }); + it('sets adTemplate', () => { + adUnit.nativeParams.adTemplate = 'template'; + setNativeResponseProperties(bid, adUnit); + expect(bid.native.adTemplate).to.eql('template'); }); }); }); @@ -722,7 +718,7 @@ describe('validate native openRTB', function () { describe('validate native', function () { const adUnit = { - transactionId: 'test_adunit', + adUnitId: 'test_adunit', mediaTypes: { native: { title: { @@ -747,7 +743,7 @@ describe('validate native', function () { let validBid = { adId: 'abc123', requestId: 'test_bid_id', - transactionId: 'test_adunit', + adUnitId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -774,7 +770,7 @@ describe('validate native', function () { let noIconDimBid = { adId: 'abc234', requestId: 'test_bid_id', - transactionId: 'test_adunit', + adUnitId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -797,7 +793,7 @@ describe('validate native', function () { let noImgDimBid = { adId: 'abc345', requestId: 'test_bid_id', - transactionId: 'test_adunit', + adUnitId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -834,7 +830,7 @@ describe('validate native', function () { it('should convert from old-style native to OpenRTB request', () => { const adUnit = { - transactionId: 'test_adunit', + adUnitId: 'test_adunit', mediaTypes: { native: { title: { @@ -860,6 +856,9 @@ describe('validate native', function () { }] }, address: {}, + privacyLink: { + required: true + } }, }, }; @@ -915,6 +914,7 @@ describe('validate native', function () { type: 9, } }); + expect(ortb.privacy).to.equal(1); }); ['bogusKey', 'clickUrl', 'privacyLink'].forEach(nativeKey => { @@ -1022,11 +1022,14 @@ describe('validate native', function () { expect(oldNativeRequest.sponsoredBy).to.include({ required: true, len: 25 - }) + }); expect(oldNativeRequest.body).to.include({ required: true, len: 140 - }) + }); + expect(oldNativeRequest.privacyLink).to.include({ + required: false + }); }); if (FEATURES.NATIVE) { @@ -1034,7 +1037,7 @@ describe('validate native', function () { const validBidRequests = [{ bidId: 'bidId3', adUnitCode: 'adUnitCode3', - transactionId: 'transactionId3', + adUnitId: 'transactionId3', mediaTypes: { banner: {} }, @@ -1197,6 +1200,12 @@ describe('legacyPropertiesToOrtbNative', () => { expect(native.jstracker).to.eql('some-markupsome-other-markup'); }) }); + describe('privacylink', () => { + it('should convert privacyLink to privacy', () => { + const native = legacyPropertiesToOrtbNative({privacyLink: 'https:/my-privacy-link.com'}); + expect(native.privacy).to.eql('https:/my-privacy-link.com'); + }) + }) }); describe('fireImpressionTrackers', () => { diff --git a/test/spec/ortbConverter/common_spec.js b/test/spec/ortbConverter/common_spec.js new file mode 100644 index 00000000000..d2d61e6778c --- /dev/null +++ b/test/spec/ortbConverter/common_spec.js @@ -0,0 +1,29 @@ +import {DEFAULT_PROCESSORS} from '../../../libraries/ortbConverter/processors/default.js'; +import {BID_RESPONSE} from '../../../src/pbjsORTB.js'; + +describe('common processors', () => { + describe('bid response properties', () => { + const responseProps = DEFAULT_PROCESSORS[BID_RESPONSE].props.fn; + let context; + + beforeEach(() => { + context = { + ortbResponse: {} + } + }) + + describe('meta.dsa', () => { + const MOCK_DSA = {transparency: 'info'}; + it('is not set if bid has no meta.dsa', () => { + const resp = {}; + responseProps(resp, {}, context); + expect(resp.meta?.dsa).to.not.exist; + }); + it('is set to ext.dsa otherwise', () => { + const resp = {}; + responseProps(resp, {ext: {dsa: MOCK_DSA}}, context); + expect(resp.meta.dsa).to.eql(MOCK_DSA); + }) + }) + }) +}) diff --git a/test/spec/unit/adRendering_spec.js b/test/spec/unit/adRendering_spec.js new file mode 100644 index 00000000000..c2f62842c7e --- /dev/null +++ b/test/spec/unit/adRendering_spec.js @@ -0,0 +1,248 @@ +import * as events from 'src/events.js'; +import * as utils from 'src/utils.js'; +import { + doRender, + getRenderingData, + handleCreativeEvent, + handleNativeMessage, + handleRender +} from '../../../src/adRendering.js'; +import CONSTANTS from 'src/constants.json'; +import {expect} from 'chai/index.mjs'; +import {config} from 'src/config.js'; +import {VIDEO} from '../../../src/mediaTypes.js'; +import {auctionManager} from '../../../src/auctionManager.js'; + +describe('adRendering', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(utils, 'logWarn'); + sandbox.stub(utils, 'logError'); + }) + afterEach(() => { + sandbox.restore(); + }) + + describe('getRenderingData', () => { + let bidResponse; + beforeEach(() => { + bidResponse = {}; + }); + + ['ad', 'adUrl'].forEach((prop) => { + describe(`on ${prop}`, () => { + it('replaces AUCTION_PRICE macro', () => { + bidResponse[prop] = 'pre${AUCTION_PRICE}post'; + bidResponse.cpm = 123; + const result = getRenderingData(bidResponse); + expect(result[prop]).to.eql('pre123post'); + }); + it('replaces CLICKTHROUGH macro', () => { + bidResponse[prop] = 'pre${CLICKTHROUGH}post'; + const result = getRenderingData(bidResponse, {clickUrl: 'clk'}); + expect(result[prop]).to.eql('preclkpost'); + }); + it('defaults CLICKTHROUGH to empty string', () => { + bidResponse[prop] = 'pre${CLICKTHROUGH}post'; + const result = getRenderingData(bidResponse); + expect(result[prop]).to.eql('prepost'); + }); + }); + }); + }) + + describe('rendering logic', () => { + let bidResponse, renderFn, resizeFn, adId; + beforeEach(() => { + sandbox.stub(events, 'emit'); + renderFn = sinon.stub(); + resizeFn = sinon.stub(); + adId = 123; + bidResponse = { + adId + } + }); + + function expectAdRenderFailedEvent(reason) { + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.AD_RENDER_FAILED, sinon.match({adId, reason})); + } + + describe('doRender', () => { + let getRenderingDataStub; + function getRenderingDataHook(next, ...args) { + next.bail(getRenderingDataStub(...args)); + } + before(() => { + getRenderingData.before(getRenderingDataHook, 999); + }) + after(() => { + getRenderingData.getHooks({hook: getRenderingDataHook}).remove(); + }); + beforeEach(() => { + getRenderingDataStub = sinon.stub(); + }) + + describe('when the ad has a renderer', () => { + let bidResponse; + beforeEach(() => { + bidResponse = { + adId: 'mock-ad-id', + renderer: { + url: 'some-custom-renderer', + render: sinon.stub() + } + } + }); + + it('does not invoke renderFn, but the renderer instead', () => { + doRender({renderFn, bidResponse}); + sinon.assert.notCalled(renderFn); + sinon.assert.called(bidResponse.renderer.render); + }); + + it('emits AD_RENDER_SUCCEDED', () => { + doRender({renderFn, bidResponse}); + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, sinon.match({ + bid: bidResponse, + adId: bidResponse.adId + })); + }); + }); + + if (FEATURES.VIDEO) { + it('should emit AD_RENDER_FAILED on video bids', () => { + bidResponse.mediaType = VIDEO; + doRender({renderFn, bidResponse}); + expectAdRenderFailedEvent(CONSTANTS.AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT) + }); + } + + it('invokes renderFn with rendering data', () => { + const data = {ad: 'creative'}; + getRenderingDataStub.returns(data); + doRender({renderFn, resizeFn, bidResponse}); + sinon.assert.calledWith(renderFn, sinon.match({ + adId: bidResponse.adId, + ...data + })) + }); + + it('invokes resizeFn with w/h from rendering data', () => { + getRenderingDataStub.returns({width: 123, height: 321}); + doRender({renderFn, resizeFn, bidResponse}); + sinon.assert.calledWith(resizeFn, 123, 321); + }); + + it('does not invoke resizeFn if rendering data has no w/h', () => { + getRenderingDataStub.returns({}); + doRender({renderFn, resizeFn, bidResponse}); + sinon.assert.notCalled(resizeFn); + }) + }); + + describe('handleRender', () => { + let doRenderStub + function doRenderHook(next, ...args) { + next.bail(doRenderStub(...args)); + } + before(() => { + doRender.before(doRenderHook, 999); + }) + after(() => { + doRender.getHooks({hook: doRenderHook}).remove(); + }) + beforeEach(() => { + sandbox.stub(auctionManager, 'addWinningBid'); + doRenderStub = sinon.stub(); + }) + describe('should emit AD_RENDER_FAILED', () => { + it('when bidResponse is missing', () => { + handleRender({adId}); + expectAdRenderFailedEvent(CONSTANTS.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD); + sinon.assert.notCalled(doRenderStub); + }); + it('on exceptions', () => { + doRenderStub.throws(new Error()); + handleRender({adId, bidResponse}); + expectAdRenderFailedEvent(CONSTANTS.AD_RENDER_FAILED_REASON.EXCEPTION); + }); + }) + + describe('when bid was already rendered', () => { + beforeEach(() => { + bidResponse.status = CONSTANTS.BID_STATUS.RENDERED; + }); + afterEach(() => { + config.resetConfig(); + }) + it('should emit STALE_RENDER', () => { + handleRender({adId, bidResponse}); + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.STALE_RENDER, bidResponse); + sinon.assert.called(doRenderStub); + }); + it('should skip rendering if suppressStaleRender', () => { + config.setConfig({auctionOptions: {suppressStaleRender: true}}); + handleRender({adId, bidResponse}); + sinon.assert.notCalled(doRenderStub); + }) + }); + + it('should mark bid as won and emit BID_WON', () => { + handleRender({renderFn, bidResponse}); + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.BID_WON, bidResponse); + sinon.assert.calledWith(auctionManager.addWinningBid, bidResponse); + }) + }) + }) + + describe('handleCreativeEvent', () => { + let bid; + beforeEach(() => { + sandbox.stub(events, 'emit'); + bid = { + status: CONSTANTS.BID_STATUS.RENDERED + } + }); + it('emits AD_RENDER_FAILED with given reason', () => { + handleCreativeEvent({event: CONSTANTS.EVENTS.AD_RENDER_FAILED, info: {reason: 'reason', message: 'message'}}, bid); + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.AD_RENDER_FAILED, sinon.match({bid, reason: 'reason', message: 'message'})); + }); + + it('emits AD_RENDER_SUCCEEDED', () => { + handleCreativeEvent({event: CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED}, bid); + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, sinon.match({bid})); + }); + + it('logs an error on other events', () => { + handleCreativeEvent({event: 'unsupported'}, bid); + sinon.assert.called(utils.logError); + sinon.assert.notCalled(events.emit); + }); + }); + + describe('handleNativeMessage', () => { + if (!FEATURES.NATIVE) return; + let bid; + beforeEach(() => { + bid = { + adId: '123' + }; + }) + + it('should resize', () => { + const resizeFn = sinon.stub(); + handleNativeMessage({action: 'resizeNativeHeight', height: 100}, bid, {resizeFn}); + sinon.assert.calledWith(resizeFn, undefined, 100); + }); + + it('should fire trackers', () => { + const data = { + action: 'click' + }; + const fireTrackers = sinon.stub(); + handleNativeMessage(data, bid, {fireTrackers}); + sinon.assert.calledWith(fireTrackers, data, bid); + }) + }) +}); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index cff26df2e4d..dac70696b4b 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -964,15 +964,42 @@ describe('adapterManager tests', function () { 'start': 1462918897460 }]; - it('invokes callBids on the S2S adapter', function () { - adapterManager.callBids( - getAdUnits(), - bidRequests, - () => {}, - () => () => {} - ); - sinon.assert.calledTwice(prebidServerAdapterMock.callBids); - }); + describe('invokes callBids on the S2S adapter', () => { + let onTimelyResponse, timedOut, done; + beforeEach(() => { + done = sinon.stub(); + onTimelyResponse = sinon.stub(); + prebidServerAdapterMock.callBids.callsFake((_1, _2, _3, done) => { + done(timedOut); + }); + }) + + function runTest() { + adapterManager.callBids( + getAdUnits(), + bidRequests, + () => {}, + done, + undefined, + undefined, + onTimelyResponse + ); + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + sinon.assert.calledTwice(done); + } + + it('and marks requests as timely if the adapter says timedOut = false', function () { + timedOut = false; + runTest(); + bidRequests.forEach(br => sinon.assert.calledWith(onTimelyResponse, br.bidderRequestId)); + }); + + it('and does NOT mark them as timely if it says timedOut = true', () => { + timedOut = true; + runTest(); + sinon.assert.notCalled(onTimelyResponse); + }) + }) // Enable this test when prebidServer adapter is made 1.0 compliant it('invokes callBids with only s2s bids', function () { diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js new file mode 100644 index 00000000000..dd03ad1a761 --- /dev/null +++ b/test/spec/unit/core/ajax_spec.js @@ -0,0 +1,429 @@ +import {attachCallbacks, dep, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {config} from 'src/config.js'; +import {server} from '../../../mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {logError} from 'src/utils.js'; + +const EXAMPLE_URL = 'https://www.example.com'; + +describe('fetcherFactory', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + server.autoTimeout = true; + }); + + afterEach(() => { + clock.runAll(); + clock.restore(); + config.resetConfig(); + }); + + Object.entries({ + 'URL': EXAMPLE_URL, + 'request object': new Request(EXAMPLE_URL) + }).forEach(([t, resource]) => { + it(`times out after timeout when fetching ${t}`, (done) => { + const fetch = fetcherFactory(1000); + const resp = fetch(resource); + clock.tick(900); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + clock.tick(100); + expect(server.requests[0].fetch.request.signal.aborted).to.be.true; + resp.catch(() => done()); + }); + }); + + it('does not timeout after it completes', () => { + const fetch = fetcherFactory(1000); + const resp = fetch(EXAMPLE_URL); + server.requests[0].respond(); + return resp.then(() => { + clock.tick(2000); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + }); + }); + + Object.entries({ + 'disableAjaxTimeout is set'() { + const fetcher = fetcherFactory(1000); + config.setConfig({disableAjaxTimeout: true}); + return fetcher; + }, + 'timeout is null'() { + return fetcherFactory(null); + }, + }).forEach(([t, mkFetcher]) => { + it(`does not timeout if ${t}`, (done) => { + const fetch = mkFetcher(); + const pm = fetch(EXAMPLE_URL); + clock.tick(2000); + server.requests[0].respond(); + pm.then(() => done()); + }); + }); + + Object.entries({ + 'local URL': ['/local.html', window.origin], + 'remote URL': [EXAMPLE_URL + '/remote.html', EXAMPLE_URL], + 'request with local URL': [new Request('/local.html'), window.origin], + 'request with remote URL': [new Request(EXAMPLE_URL + '/remote.html'), EXAMPLE_URL] + }).forEach(([t, [resource, expectedOrigin]]) => { + describe(`using ${t}`, () => { + it('calls request, passing origin', () => { + const request = sinon.stub(); + const fetch = fetcherFactory(1000, {request}); + fetch(resource); + sinon.assert.calledWith(request, expectedOrigin); + }); + + Object.entries({ + success: 'respond', + error: 'error' + }).forEach(([t, method]) => { + it(`calls done on ${t}, passing origin`, () => { + const done = sinon.stub(); + const fetch = fetcherFactory(1000, {done}); + const req = fetch(resource).catch(() => null).then(() => { + sinon.assert.calledWith(done, expectedOrigin); + }); + server.requests[0][method](); + return req; + }); + }); + }); + }); +}); + +describe('toFetchRequest', () => { + Object.entries({ + 'simple POST': { + url: EXAMPLE_URL, + data: 'data', + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: 'data', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'POST with headers': { + url: EXAMPLE_URL, + data: '{"json": "body"}', + options: { + contentType: 'application/json', + customHeaders: { + 'x-custom': 'value' + } + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: '{"json": "body"}', + headers: { + 'content-type': 'application/json', + 'X-Custom': 'value' + } + } + }, + 'simple GET': { + url: EXAMPLE_URL, + data: {p1: 'v1', p2: 'v2'}, + options: { + method: 'GET', + }, + expect: { + request: { + url: EXAMPLE_URL + '/?p1=v1&p2=v2', + method: 'GET' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'GET with credentials': { + url: EXAMPLE_URL, + data: null, + options: { + method: 'GET', + withCredentials: true, + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'GET', + credentials: 'include' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + } + }).forEach(([t, {url, data, options, expect: {request, text, headers}}]) => { + it(`can build ${t}`, () => { + const req = toFetchRequest(url, data, options); + return req.text().then(body => { + Object.entries(request).forEach(([prop, val]) => { + expect(req[prop]).to.eql(val); + }); + const hdr = new Headers(headers); + Array.from(req.headers.entries()).forEach(([name, val]) => { + expect(hdr.get(name)).to.eql(val); + }); + expect(body).to.eql(text); + }); + }); + }); + + describe('browsingTopics', () => { + Object.entries({ + 'browsingTopics = true': [{browsingTopics: true}, true], + 'browsingTopics = false': [{browsingTopics: false}, false], + 'browsingTopics is undef': [{}, false] + }).forEach(([t, [opts, shouldBeSet]]) => { + describe(`when options has ${t}`, () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + }); + + it(`should ${!shouldBeSet ? 'not ' : ''}be set when in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => true); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + }); + it(`should not be set when not in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => false); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: undefined}); + }); + }) + }) + }) +}); + +describe('attachCallbacks', () => { + const sampleHeaders = new Headers({ + 'x-1': 'v1', + 'x-2': 'v2' + }); + + function responseFactory(body, props) { + props = Object.assign({headers: sampleHeaders, url: EXAMPLE_URL}, props); + return function () { + return { + response: Object.defineProperties(new Response(body, props), { + url: { + get: () => props.url + } + }), + body: body || '' + }; + }; + } + + function expectNullXHR(response, reason) { + return new Promise((resolve, reject) => { + attachCallbacks(Promise.resolve(response), { + success: () => { + reject(new Error('should not succeed')); + }, + error(statusText, xhr) { + expect(statusText).to.eql(''); + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: 0, + statusText: '', + responseText: '', + response: '', + responseXML: null, + reason + }); + expect(xhr.getResponseHeader('any')).to.be.null; + resolve(); + } + }); + }); + } + + it('runs error callback on rejections', () => { + const err = new Error(); + return expectNullXHR(Promise.reject(err), err); + }); + + it('sets timedOut = true on fetch timeout', (done) => { + const ctl = new AbortController(); + ctl.abort(); + attachCallbacks(fetch('/', {signal: ctl.signal}), { + error(_, xhr) { + expect(xhr.timedOut).to.be.true; + done(); + } + }); + }) + + Object.entries({ + '2xx response': { + success: true, + makeResponse: responseFactory('body', {status: 200, statusText: 'OK'}) + }, + '2xx response with no body': { + success: true, + makeResponse: responseFactory(null, {status: 204, statusText: 'No content'}) + }, + '2xx response with XML': { + success: true, + xml: true, + makeResponse: responseFactory('', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'application/xml;charset=UTF8'} + }) + }, + '2xx response with HTML': { + success: true, + xml: true, + makeResponse: responseFactory('

', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'text/html;charset=UTF-8'} + }) + }, + '304 response': { + success: true, + makeResponse: responseFactory(null, {status: 304, statusText: 'Moved permanently'}) + }, + '4xx response': { + success: false, + makeResponse: responseFactory('body', {status: 400, statusText: 'Invalid request'}) + }, + '5xx response': { + success: false, + makeResponse: responseFactory('body', {status: 503, statusText: 'Gateway error'}) + }, + '4xx response with XML': { + success: false, + xml: true, + makeResponse: responseFactory('', { + status: 404, + statusText: 'Not found', + headers: { + 'content-type': 'application/xml' + } + }) + } + }).forEach(([t, {success, makeResponse, xml}]) => { + const cbType = success ? 'success' : 'error'; + + describe(`for ${t}`, () => { + let sandbox, response, body; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logError'); + ({response, body} = makeResponse()); + }); + + afterEach(() => { + sandbox.restore(); + }) + + function checkXHR(xhr) { + utils.logError.resetHistory(); + const serialized = JSON.parse(JSON.stringify(xhr)) + // serialization of `responseXML` should not generate console messages + sinon.assert.notCalled(utils.logError); + + sinon.assert.match(serialized, { + readyState: XMLHttpRequest.DONE, + status: response.status, + statusText: response.statusText, + responseType: '', + responseURL: response.url, + response: body, + responseText: body, + }); + if (xml) { + expect(xhr.responseXML.querySelectorAll('*').length > 0).to.be.true; + } else { + expect(serialized.responseXML).to.not.exist; + } + Array.from(response.headers.entries()).forEach(([name, value]) => { + expect(xhr.getResponseHeader(name)).to.eql(value); + }); + expect(xhr.getResponseHeader('$$missing-header')).to.be.null; + } + + it(`runs ${cbType} callback`, (done) => { + attachCallbacks(Promise.resolve(response), { + success(payload, xhr) { + expect(success).to.be.true; + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }, + error(statusText, xhr) { + expect(success).to.be.false; + expect(statusText).to.eql(response.statusText); + checkXHR(xhr); + done(); + } + }); + }); + + it(`runs error callback if body cannot be retrieved`, () => { + const err = new Error(); + response.text = () => Promise.reject(err); + return expectNullXHR(response, err); + }); + + if (success) { + it('accepts a single function as success callback', (done) => { + attachCallbacks(Promise.resolve(response), function (payload, xhr) { + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }) + }) + } + }); + }); + + describe('callback exceptions', () => { + Object.entries({ + success: responseFactory(null, {status: 204}), + error: responseFactory('', {status: 400}), + }).forEach(([cbType, makeResponse]) => { + it(`do not choke ${cbType} callbacks`, () => { + const {response} = makeResponse(); + return new Promise((resolve) => { + const result = {success: false, error: false}; + attachCallbacks(Promise.resolve(response), { + success() { + result.success = true; + throw new Error(); + }, + error() { + result.error = true; + throw new Error(); + } + }); + setTimeout(() => resolve(result), 20); + }).then(result => { + Object.entries(result).forEach(([typ, ran]) => { + expect(ran).to.be[typ === cbType ? 'true' : 'false'] + }) + }); + }); + }); + }); +}); diff --git a/test/spec/unit/core/auctionIndex_spec.js b/test/spec/unit/core/auctionIndex_spec.js index f00e2cd281f..df29ed1a6cb 100644 --- a/test/spec/unit/core/auctionIndex_spec.js +++ b/test/spec/unit/core/auctionIndex_spec.js @@ -38,22 +38,22 @@ describe('auction index', () => { let adUnits; beforeEach(() => { - adUnits = [{transactionId: 'au1'}, {transactionId: 'au2'}]; + adUnits = [{adUnitId: 'au1'}, {adUnitId: 'au2'}]; auctions = [ mockAuction('a1', [adUnits[0], {}]), mockAuction('a2', [adUnits[1]]) ]; }); - it('should find adUnits by transactionId', () => { - expect(index.getAdUnit({transactionId: 'au2'})).to.equal(adUnits[1]); + it('should find adUnits by adUnitId', () => { + expect(index.getAdUnit({adUnitId: 'au2'})).to.equal(adUnits[1]); }); it('should return undefined if adunit is missing', () => { - expect(index.getAdUnit({transactionId: 'missing'})).to.be.undefined; + expect(index.getAdUnit({adUnitId: 'missing'})).to.be.undefined; }); - it('should return undefined if no transactionId is provided', () => { + it('should return undefined if no adUnitId is provided', () => { expect(index.getAdUnit({})).to.be.undefined; }); }); @@ -87,12 +87,12 @@ describe('auction index', () => { beforeEach(() => { mediaTypes = [{mockMT: '1'}, {mockMT: '2'}, {mockMT: '3'}, {mockMT: '4'}] adUnits = [ - {transactionId: 'au1', mediaTypes: mediaTypes[0]}, - {transactionId: 'au2', mediaTypes: mediaTypes[1]} + {adUnitId: 'au1', mediaTypes: mediaTypes[0]}, + {adUnitId: 'au2', mediaTypes: mediaTypes[1]} ] bidderRequests = [ - {bidderRequestId: 'ber1', bids: [{bidId: 'b1', mediaTypes: mediaTypes[2], transactionId: 'au1'}, {}]}, - {bidderRequestId: 'ber2', bids: [{bidId: 'b2', mediaTypes: mediaTypes[3], transactionId: 'au2'}]} + {bidderRequestId: 'ber1', bids: [{bidId: 'b1', mediaTypes: mediaTypes[2], adUnitId: 'au1'}, {}]}, + {bidderRequestId: 'ber2', bids: [{bidId: 'b2', mediaTypes: mediaTypes[3], adUnitId: 'au2'}]} ] auctions = [ mockAuction('a1', [adUnits[0]], [bidderRequests[0], {}]), @@ -100,8 +100,8 @@ describe('auction index', () => { ] }); - it('should find mediaTypes by transactionId', () => { - expect(index.getMediaTypes({transactionId: 'au2'})).to.equal(mediaTypes[1]); + it('should find mediaTypes by adUnitId', () => { + expect(index.getMediaTypes({adUnitId: 'au2'})).to.equal(mediaTypes[1]); }); it('should find mediaTypes by requestId', () => { @@ -109,18 +109,18 @@ describe('auction index', () => { }); it('should give precedence to request.mediaTypes over adUnit.mediaTypes', () => { - expect(index.getMediaTypes({requestId: 'b2', transactionId: 'au2'})).to.equal(mediaTypes[3]); + expect(index.getMediaTypes({requestId: 'b2', adUnitId: 'au2'})).to.equal(mediaTypes[3]); }); - it('should return undef if requestId and transactionId do not match', () => { - expect(index.getMediaTypes({requestId: 'b1', transactionId: 'au2'})).to.be.undefined; + it('should return undef if requestId and adUnitId do not match', () => { + expect(index.getMediaTypes({requestId: 'b1', adUnitId: 'au2'})).to.be.undefined; }); it('should return undef if no params are provided', () => { expect(index.getMediaTypes({})).to.be.undefined; }); - ['requestId', 'transactionId'].forEach(param => { + ['requestId', 'adUnitId'].forEach(param => { it(`should return undef if ${param} is missing`, () => { expect(index.getMediaTypes({[param]: 'missing'})).to.be.undefined; }); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 751c90af50f..5fe5a1accfc 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -13,9 +13,8 @@ import {stubAuctionIndex} from '../../../helpers/indexStub.js'; import {bidderSettings} from '../../../../src/bidderSettings.js'; import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; import * as activityRules from 'src/activities/rules.js'; -import {sandbox} from 'sinon'; import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../../../../src/activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../../../../src/activities/activities.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -39,133 +38,112 @@ const MOCK_BIDS_REQUEST = { ] } -function onTimelyResponseStub() { - -} - before(() => { hook.ready(); }); let wrappedCallback = config.callbackWithBidder(CODE); -describe('bidders created by newBidder', function () { - let spec; - let bidder; - let addBidResponseStub; - let doneStub; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - getUserSyncs: sinon.stub() - }; - - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - }); - - describe('when the ajax response is irrelevant', function () { - let sandbox; - let ajaxStub; - let getConfigSpy; - let aliasRegistryStub, aliasRegistry; +describe('bidderFactory', () => { + let onTimelyResponseStub; + beforeEach(() => { + onTimelyResponseStub = sinon.stub(); + }) + describe('bidders created by newBidder', function () { + let spec; + let bidder; + let addBidResponseStub; + let doneStub; beforeEach(function () { - sandbox = sinon.sandbox.create(); - sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); - ajaxStub = sandbox.stub(ajax, 'ajax'); - addBidResponseStub.reset(); - getConfigSpy = sandbox.spy(config, 'getConfig'); - doneStub.reset(); - aliasRegistry = {}; - aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + getUserSyncs: sinon.stub() + }; - afterEach(function () { - sandbox.restore(); + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); }); - it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } - }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); + describe('when the ajax response is irrelevant', function () { + let sandbox; + let ajaxStub; + let getConfigSpy; + let aliasRegistryStub, aliasRegistry; - it('should let registerSyncs run with valid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); + ajaxStub = sandbox.stub(ajax, 'ajax'); + addBidResponseStub.reset(); + getConfigSpy = sandbox.spy(config, 'getConfig'); + doneStub.reset(); + aliasRegistry = {}; + aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + afterEach(function () { + sandbox.restore(); }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - aliasRegistry = {[spec.code]: CODE}; - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); - }); - describe('transaction IDs', () => { - Object.entries({ - 'be hidden': false, - 'not be hidden': true, - }).forEach(([t, allowed]) => { - const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; + it('should let registerSyncs run with valid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); - function checkBidRequest(br) { - ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); - } + it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); - function checkBidderRequest(br) { - expectation(br.auctionId); - br.bids.forEach(checkBidRequest); - } + it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + aliasRegistry = {[spec.code]: CODE}; + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); + }); - it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { - spec.isBidRequestValid.callsFake(br => { - checkBidRequest(br); - return true; - }); - spec.buildRequests.callsFake((bidReqs, bidderReq) => { - checkBidderRequest(bidderReq); - bidReqs.forEach(checkBidRequest); - return {method: 'POST'}; - }); + describe('transaction IDs', () => { + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); spec.interpretResponse.callsFake(() => [ { requestId: 'bid', @@ -176,599 +154,929 @@ describe('bidders created by newBidder', function () { currency: 'USD' } ]) - activityRules.isActivityAllowed.reset(); - activityRules.isActivityAllowed.callsFake(() => allowed); + }); - ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); + Object.entries({ + 'be hidden': false, + 'not be hidden': true, + }).forEach(([t, allowed]) => { + const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; - const bidder = newBidder(spec); + function checkBidRequest(br) { + ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); + } + + function checkBidderRequest(br) { + expectation(br.auctionId); + br.bids.forEach(checkBidRequest); + } + + it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { + spec.isBidRequestValid.callsFake(br => { + checkBidRequest(br); + return true; + }); + spec.buildRequests.callsFake((bidReqs, bidderReq) => { + checkBidderRequest(bidderReq); + bidReqs.forEach(checkBidRequest); + return {method: 'POST'}; + }); + activityRules.isActivityAllowed.callsFake(() => allowed); + + const bidder = newBidder(spec); + + bidder.callBids({ + bidderCode: 'mockBidder', + auctionId: 'aid', + bids: [ + { + adUnitCode: 'mockAU', + bidId: 'bid', + transactionId: 'tid', + auctionId: 'aid' + } + ] + }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { + componentType: MODULE_TYPE_BIDDER, + componentName: 'mockBidder' + }); + sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { + transactionId: 'tid', + auctionId: 'aid' + }) + }); + }); - bidder.callBids({ + it('should not be hidden from request methods', (done) => { + const bidderRequest = { bidderCode: 'mockBidder', auctionId: 'aid', + getAID() { return this.auctionId }, bids: [ { adUnitCode: 'mockAU', bidId: 'bid', transactionId: 'tid', - auctionId: 'aid' + auctionId: 'aid', + getTIDs() { + return [this.auctionId, this.transactionId] + } } ] - }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { - componentType: MODULE_TYPE_BIDDER, - componentName: 'mockBidder' + }; + activityRules.isActivityAllowed.callsFake(() => false); + spec.isBidRequestValid.returns(true); + spec.buildRequests.callsFake((reqs, bidderReq) => { + expect(bidderReq.getAID()).to.eql('aid'); + expect(reqs[0].getTIDs()).to.eql(['aid', 'tid']); + done(); }); - sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { - transactionId: 'tid', - auctionId: 'aid' - }) - }); + newBidder(spec).callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + }) }); - }); - - it('should handle bad bid requests gracefully', function () { - const bidder = newBidder(spec); - spec.getUserSyncs.returns([]); + it('should handle bad bid requests gracefully', function () { + const bidder = newBidder(spec); - bidder.callBids({}); - bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.getUserSyncs.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.called).to.equal(false); - expect(spec.buildRequests.called).to.equal(false); - expect(spec.interpretResponse.called).to.equal(false); - }); + bidder.callBids({}); + bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should call buildRequests(bidRequest) the params are valid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.called).to.equal(false); + expect(spec.buildRequests.called).to.equal(false); + expect(spec.interpretResponse.called).to.equal(false); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should call buildRequests(bidRequest) the params are valid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not call buildRequests the params are invalid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); + }); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); + it('should not call buildRequests the params are invalid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should filter out invalid bids before calling buildRequests', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.called).to.equal(false); + }); - spec.isBidRequestValid.onFirstCall().returns(true); - spec.isBidRequestValid.onSecondCall().returns(false); - spec.buildRequests.returns([]); + it('should filter out invalid bids before calling buildRequests', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.onFirstCall().returns(true); + spec.isBidRequestValid.onSecondCall().returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make no server requests if the spec doesn\'t return any', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should make no server requests if the spec doesn\'t return any', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data + expect(ajaxStub.called).to.equal(false); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'POST', - contentType: 'text/plain', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const options = { contentType: 'application/json' }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data, - options: options + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'text/plain', + withCredentials: true + }); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const options = { contentType: 'application/json' }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data, + options: options + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'POST', - contentType: 'application/json', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate GET request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'application/json', + withCredentials: true + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate GET request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data + }); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'GET', - withCredentials: true + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: true + }) }); - }); - it('should make the appropriate GET request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const opt = { withCredentials: false } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data, - options: opt + it('should make the appropriate GET request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const opt = { withCredentials: false } + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data, + options: opt + }); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: false + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make multiple calls if the spec returns them', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: url, + data: data + }, + { + method: 'GET', + url: url, + data: data + } + ]); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'GET', - withCredentials: false + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(ajaxStub.calledTwice).to.equal(true); }); - }); - it('should make multiple calls if the spec returns them', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { - method: 'POST', - url: url, - data: data - }, - { - method: 'GET', - url: url, - data: data - } - ]); + describe('browsingTopics ajax option', () => { + let transmitUfpdAllowed, bidder, origBS; + before(() => { + origBS = window.$$PREBID_GLOBAL$$.bidderSettings; + }) - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + after(() => { + window.$$PREBID_GLOBAL$$.bidderSettings = origBS; + }); - expect(ajaxStub.calledTwice).to.equal(true); - }); + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); + bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + }); - it('should not add bids for each placement code if no requests are given', function () { - const bidder = newBidder(spec); + it(`should be set to false when adapter sets browsingTopics = false`, () => { + transmitUfpdAllowed = true; + spec.buildRequests.returns([ + { + method: 'GET', + url: 'url', + options: { + browsingTopics: false + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: false + })); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); + Object.entries({ + 'omitted': [undefined, true], + 'enabled': [true, true], + 'disabled': [false, false] + }).forEach(([t, [topicsHeader, enabled]]) => { + describe(`when bidderSettings.topicsHeader is ${t}`, () => { + beforeEach(() => { + window.$$PREBID_GLOBAL$$.bidderSettings = { + [CODE]: { + topicsHeader: topicsHeader + } + } + }); + + afterEach(() => { + delete window.$$PREBID_GLOBAL$$.bidderSettings[CODE]; + }); + + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, allow]) => { + const shouldBeSet = allow && enabled; + + it(`should be set to ${shouldBeSet} when transmitUfpd is ${t}`, () => { + transmitUfpdAllowed = allow; + spec.buildRequests.returns([ + { + method: 'GET', + url: '1', + }, + { + method: 'POST', + url: '2', + data: {} + }, + { + method: 'GET', + url: '3', + options: { + browsingTopics: true + } + }, + { + method: 'POST', + url: '4', + data: {}, + options: { + browsingTopics: true + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + ['1', '2', '3', '4'].forEach(url => { + sinon.assert.calledWith( + ajaxStub, + url, + sinon.match.any, + sinon.match.any, + sinon.match({browsingTopics: shouldBeSet}) + ); + }); + }); + }); + }) + }) + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not add bids for each placement code if no requests are given', function () { + const bidder = newBidder(spec); - expect(addBidResponseStub.callCount).to.equal(0); - }); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); - it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { - const bidder = newBidder(spec); - const req = { - method: 'POST', - url: 'test.url.com', - data: { arg: 2 } - }; + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([req, req]); + expect(addBidResponseStub.callCount).to.equal(0); + }); - const eventEmitterSpy = sinon.spy(events, 'emit'); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { + const bidder = newBidder(spec); + const req = { + method: 'POST', + url: 'test.url.com', + data: { arg: 2 } + }; - expect(ajaxStub.calledTwice).to.equal(true); - expect(eventEmitterSpy.getCalls() - .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) - ).to.length(2); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([req, req]); - eventEmitterSpy.restore(); - }); - }); + const eventEmitterSpy = sinon.spy(events, 'emit'); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - describe('when the ajax call succeeds', function () { - let ajaxStub; - let userSyncStub; - let logErrorSpy; + expect(ajaxStub.calledTwice).to.equal(true); + expect(eventEmitterSpy.getCalls() + .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) + ).to.length(2); - beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); + eventEmitterSpy.restore(); }); - addBidResponseStub.reset(); - doneStub.resetBehavior(); - userSyncStub = sinon.stub(userSync, 'registerSync') - logErrorSpy = sinon.spy(utils, 'logError'); }); - afterEach(function () { - ajaxStub.restore(); - userSyncStub.restore(); - utils.logError.restore(); - }); + describe('when the ajax call succeeds', function () { + let ajaxStub; + let userSyncStub; + let logErrorSpy; - it('should call spec.interpretResponse() with the response content', function () { - const bidder = newBidder(spec); + beforeEach(function () { + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); + }); + addBidResponseStub.reset(); + doneStub.resetBehavior(); + userSyncStub = sinon.stub(userSync, 'registerSync') + logErrorSpy = sinon.spy(utils, 'logError'); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + afterEach(function () { + ajaxStub.restore(); + userSyncStub.restore(); + utils.logError.restore(); }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call onTimelyResponse', () => { + const bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({method: 'POST', url: 'test', data: {}}); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.called(onTimelyResponseStub); + }) - expect(spec.interpretResponse.calledOnce).to.equal(true); - const response = spec.interpretResponse.firstCall.args[0] - expect(response.body).to.equal('response body') - expect(response.headers.get('some-header')).to.equal('headerContent'); - expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ - method: 'POST', - url: 'test.url.com', - data: {} + it('should call spec.interpretResponse() with the response content', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledOnce).to.equal(true); + const response = spec.interpretResponse.firstCall.args[0] + expect(response.body).to.equal('response body') + expect(response.headers.get('some-header')).to.equal('headerContent'); + expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + expect(doneStub.calledOnce).to.equal(true); }); - expect(doneStub.calledOnce).to.equal(true); - }); - it('should call spec.interpretResponse() once for each request made', function () { - const bidder = newBidder(spec); + it('should call spec.interpretResponse() once for each request made', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + ]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledTwice).to.equal(true); + expect(doneStub.calledOnce).to.equal(true); + }); + + it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { + const bidder = newBidder(spec); + + const bid = { + creativeId: 'creative-id', + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + adUnitCode: 'mock/placement', + currency: 'USD', + netRevenue: true, + ttl: 300, + bidderCode: 'sampleBidder', + sampleBidder: {advertiserId: '12345', networkId: '111222'} + }; + const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); + bidderRequest.bids[0].bidder = 'sampleBidder'; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - { + }); + spec.getUserSyncs.returns([]); + + spec.interpretResponse.returns(bid); + + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + let bidObject = addBidResponseStub.firstCall.args[1]; + // checking the fields added by our code + expect(bidObject.originalCpm).to.equal(bid.cpm); + expect(bidObject.originalCurrency).to.equal(bid.currency); + expect(doneStub.calledOnce).to.equal(true); + expect(logErrorSpy.callCount).to.equal(0); + expect(bidObject.meta).to.exist; + expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); + }); + + it('should call spec.getUserSyncs() with the response', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - ]); - spec.getUserSyncs.returns([]); + }); + spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(spec.interpretResponse.calledTwice).to.equal(true); - expect(doneStub.calledOnce).to.equal(true); - }); + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); + expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); + }); - it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { - const bidder = newBidder(spec); + it('should register usersync pixels', function () { + const bidder = newBidder(spec); - const bid = { - creativeId: 'creative-id', - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - adUnitCode: 'mock/placement', - currency: 'USD', - netRevenue: true, - ttl: 300, - bidderCode: 'sampleBidder', - sampleBidder: {advertiserId: '12345', networkId: '111222'} - }; - const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); - bidderRequest.bids[0].bidder = 'sampleBidder'; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); + spec.getUserSyncs.returns([{ + type: 'iframe', + url: 'usersync.com' + }]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(userSyncStub.called).to.equal(true); + expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); + expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); + expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should logError and reject bid when required bid response params are missing', function () { + const bidder = newBidder(spec); - bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const bid = { + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + placementCode: 'mock/placement' + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - let bidObject = addBidResponseStub.firstCall.args[1]; - // checking the fields added by our code - expect(bidObject.originalCpm).to.equal(bid.cpm); - expect(bidObject.originalCurrency).to.equal(bid.currency); - expect(doneStub.calledOnce).to.equal(true); - expect(logErrorSpy.callCount).to.equal(0); - expect(bidObject.meta).to.exist; - expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); - }); + spec.interpretResponse.returns(bid); - it('should call spec.getUserSyncs() with the response', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should logError and reject bid when required response params are undefined', function () { + const bidder = newBidder(spec); - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); - expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); - }); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': undefined, + 'netRevenue': true, + 'ttl': 360 + }; + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - it('should register usersync pixels', function () { - const bidder = newBidder(spec); + spec.interpretResponse.returns(bid); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); - spec.getUserSyncs.returns([{ - type: 'iframe', - url: 'usersync.com' - }]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - expect(userSyncStub.called).to.equal(true); - expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); - expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); - expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); - }); + it('should require requestId from interpretResponse', () => { + const bidder = newBidder(spec); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + spec.interpretResponse.returns(bid); - it('should logError and reject bid when required bid response params are missing', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bid = { - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - placementCode: 'mock/placement' - }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(addBidResponseStub.called).to.be.false; + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - spec.getUserSyncs.returns([]); + }); - spec.interpretResponse.returns(bid); + describe('when the ajax call fails', function () { + let ajaxStub; + let callBidderErrorStub; + let eventEmitterStub; + let xhrErrorMock; - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + beforeEach(function () { + xhrErrorMock = { + status: 500, + statusText: 'Internal Server Error' + }; + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + callbacks.error('ajax call failed.', xhrErrorMock); + }); + callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); + eventEmitterStub = sinon.stub(events, 'emit'); + addBidResponseStub.reset(); + doneStub.reset(); + }); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + afterEach(function () { + ajaxStub.restore(); + callBidderErrorStub.restore(); + eventEmitterStub.restore(); + }); - it('should logError and reject bid when required response params are undefined', function () { - const bidder = newBidder(spec); + Object.entries({ + 'timeouts': true, + 'other errors': false + }).forEach(([t, timedOut]) => { + it(`should ${timedOut ? 'NOT ' : ''}call onTimelyResponse on ${t}`, () => { + Object.assign(xhrErrorMock, {timedOut}); + const bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({method: 'POST', url: 'test', data: {}}); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert[timedOut ? 'notCalled' : 'called'](onTimelyResponseStub); + }) + }) - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': undefined, - 'netRevenue': true, - 'ttl': 360 - }; + it('should not spec.interpretResponse()', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.called).to.equal(false); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should not add bids for each adunit code into the auction', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.callCount).to.equal(0); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - it('should require requestId from interpretResponse', () => { - const bidder = newBidder(spec); - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - expect(addBidResponseStub.called).to.be.false; - expect(addBidResponseStub.reject.calledOnce).to.be.true; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); }); }); - describe('when the ajax call fails', function () { - let ajaxStub; - let callBidderErrorStub; - let eventEmitterStub; - let xhrErrorMock = { - status: 500, - statusText: 'Internal Server Error' - }; + describe('registerBidder', function () { + let registerBidAdapterStub; + let aliasBidAdapterStub; beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - callbacks.error('ajax call failed.', xhrErrorMock); - }); - callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); - eventEmitterStub = sinon.stub(events, 'emit'); - addBidResponseStub.reset(); - doneStub.reset(); + registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); + aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); }); afterEach(function () { - ajaxStub.restore(); - callBidderErrorStub.restore(); - eventEmitterStub.restore(); + registerBidAdapterStub.restore(); + aliasBidAdapterStub.restore(); }); - it('should not spec.interpretResponse()', function () { - const bidder = newBidder(spec); + function newEmptySpec() { + return { + code: CODE, + isBidRequestValid: function() { }, + buildRequests: function() { }, + interpretResponse: function() { }, + }; + } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.interpretResponse.called).to.equal(false); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); - }); + it('should register a bidder with the adapterManager', function () { + registerBidder(newEmptySpec()); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); + expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - it('should not add bids for each adunit code into the auction', function () { - const bidder = newBidder(spec); + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.callCount).to.equal(0); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + it('should register a bidder with the appropriate mediaTypes', function () { + const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); + registerBidder(thisSpec); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register bidders with the appropriate aliases', function () { + const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); + registerBidder(thisSpec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + expect(registerBidAdapterStub.calledThrice).to.equal(true); + + // Make sure our later calls don't override the bidder code from previous calls. + expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') + + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register alias with their gvlid', function() { + const aliases = [ + { + code: 'foo', + gvlid: 1 + }, + { + code: 'bar', + gvlid: 2 + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); + }) + + it('should register alias with skipPbsAliasing', function() { + const aliases = [ + { + code: 'foo', + skipPbsAliasing: true + }, + { + code: 'bar', + skipPbsAliasing: false + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); + }) + }) + + describe('validate bid response: ', function () { + let spec; + let indexStub, adUnits, bidderRequests; + let addBidResponseStub; + let doneStub; + let ajaxStub; + let logErrorSpy; + + let bids = [{ + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }]; + + beforeEach(function () { + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + }; spec.isBidRequestValid.returns(true); spec.buildRequests.returns({ @@ -776,212 +1084,138 @@ describe('bidders created by newBidder', function () { url: 'test.url.com', data: {} }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST + + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); }); + logErrorSpy = sinon.spy(utils, 'logError'); + indexStub = sinon.stub(auctionManager, 'index'); + adUnits = []; + bidderRequests = []; + indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) }); - }); -}); -describe('registerBidder', function () { - let registerBidAdapterStub; - let aliasBidAdapterStub; - - beforeEach(function () { - registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); - aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); - }); - - afterEach(function () { - registerBidAdapterStub.restore(); - aliasBidAdapterStub.restore(); - }); - - function newEmptySpec() { - return { - code: CODE, - isBidRequestValid: function() { }, - buildRequests: function() { }, - interpretResponse: function() { }, - }; - } - - it('should register a bidder with the adapterManager', function () { - registerBidder(newEmptySpec()); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); - expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; - }); - - it('should register a bidder with the appropriate mediaTypes', function () { - const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); - registerBidder(thisSpec); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); - }); - - it('should register bidders with the appropriate aliases', function () { - const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); - registerBidder(thisSpec); - - expect(registerBidAdapterStub.calledThrice).to.equal(true); - - // Make sure our later calls don't override the bidder code from previous calls. - expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') - }); + afterEach(function () { + ajaxStub.restore(); + logErrorSpy.restore(); + indexStub.restore; + }); - it('should register alias with their gvlid', function() { - const aliases = [ - { - code: 'foo', - gvlid: 1 - }, - { - code: 'bar', - gvlid: 2 - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + if (FEATURES.NATIVE) { + it('should add native bids that do have required assets', function () { + adUnits = [{ + adUnitId: 'au', + nativeParams: { + title: {'required': true}, + } + }] + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + adUnitId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + 'mediaType': 'native', + 'native': { + 'title': 'Native Creative', + 'clickUrl': 'https://www.link.example', + } + } + ); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); - }) + const bidder = newBidder(spec); - it('should register alias with skipPbsAliasing', function() { - const aliases = [ - { - code: 'foo', - skipPbsAliasing: true - }, - { - code: 'bar', - skipPbsAliasing: false - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); - }) -}) + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); -describe('validate bid response: ', function () { - let spec; - let indexStub, adUnits, bidderRequests; - let addBidResponseStub; - let doneStub; - let ajaxStub; - let logErrorSpy; - - let bids = [{ - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }]; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - }; - - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); + it('should not add native bids that do not have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + }, + }]; + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: CODE, + mediaType: 'native', + native: { + title: undefined, + clickUrl: 'https://www.link.example', + } + } + ); - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); - }); - logErrorSpy = sinon.spy(utils, 'logError'); - indexStub = sinon.stub(auctionManager, 'index'); - adUnits = []; - bidderRequests = []; - indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - afterEach(function () { - ajaxStub.restore(); - logErrorSpy.restore(); - indexStub.restore; - }); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + }); + } - if (FEATURES.NATIVE) { - it('should add native bids that do have required assets', function () { + it('should add bid when renderer is present on outstream bids', function () { adUnits = [{ transactionId: 'au', - nativeParams: { - title: {'required': true}, + mediaTypes: { + video: {context: 'outstream'} } }] - decorateAdUnitsWithNativeParams(adUnits); let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', transactionId: 'au', + adUnitCode: 'mock/placement', params: { param: 5 }, - mediaType: 'native', }] }; let bids1 = Object.assign({}, bids[0], { - 'mediaType': 'native', - 'native': { - 'title': 'Native Creative', - 'clickUrl': 'https://www.link.example', - } + bidderCode: CODE, + mediaType: 'video', + renderer: {render: () => true, url: 'render.js'}, } ); @@ -995,394 +1229,363 @@ describe('validate bid response: ', function () { expect(logErrorSpy.callCount).to.equal(0); }); - it('should not add native bids that do not have required assets', function () { - adUnits = [{ - transactionId: 'au', - nativeParams: { - title: {'required': true}, - }, - }]; - decorateAdUnitsWithNativeParams(adUnits); + it('should add banner bids that have no width or height but single adunit size', function () { let bidRequest = { bids: [{ + bidder: CODE, bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', - transactionId: 'au', params: { param: 5 }, - mediaType: 'native', + sizes: [[300, 250]], }] }; + bidderRequests = [bidRequest]; let bids1 = Object.assign({}, bids[0], { - bidderCode: CODE, - mediaType: 'native', - native: { - title: undefined, - clickUrl: 'https://www.link.example', - } + width: undefined, + height: undefined } ); const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); }); - } - - it('should add bid when renderer is present on outstream bids', function () { - adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }] - let bidRequest = { - bids: [{ - bidId: '1', - auctionId: 'first-bid-id', - transactionId: 'au', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - }] - }; - - let bids1 = Object.assign({}, - bids[0], - { - bidderCode: CODE, - mediaType: 'video', - renderer: {render: () => true, url: 'render.js'}, - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - it('should add banner bids that have no width or height but single adunit size', function () { - let bidRequest = { - bids: [{ - bidder: CODE, - bidId: '1', - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - sizes: [[300, 250]], - }] - }; - bidderRequests = [bidRequest]; - let bids1 = Object.assign({}, - bids[0], - { - width: undefined, - height: undefined - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - describe(' Check for alternateBiddersList ', function() { - let bidRequest; - let bids1; - let logWarnSpy; - let bidderSettingStub, aliasRegistryStub; - let aliasRegistry; - - beforeEach(function () { - bidRequest = { + it('should disregard auctionId/transactionId set by the adapter', () => { + let bidderRequest = { bids: [{ - bidId: '1', bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', + bidId: '1', + auctionId: 'aid', + transactionId: 'tid', + adUnitCode: 'au', }] }; + const bidder = newBidder(spec); + spec.interpretResponse.returns(Object.assign({}, bids[0], {transactionId: 'ignored', auctionId: 'ignored'})); + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(addBidResponseStub, sinon.match.any, sinon.match({ + transactionId: 'tid', + auctionId: 'aid' + })); + }) - bids1 = Object.assign({}, - bids[0], - { - bidderCode: 'validalternatebidder', - adapterCode: 'knownadapter1' - } - ); - logWarnSpy = sinon.spy(utils, 'logWarn'); - bidderSettingStub = sinon.stub(bidderSettings, 'get'); - aliasRegistry = {}; - aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + describe(' Check for alternateBiddersList ', function() { + let bidRequest; + let bids1; + let logWarnSpy; + let bidderSettingStub, aliasRegistryStub; + let aliasRegistry; - afterEach(function () { - logWarnSpy.restore(); - bidderSettingStub.restore(); - aliasRegistryStub.restore(); - }); + beforeEach(function () { + bidRequest = { + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + + bids1 = Object.assign({}, + bids[0], + { + bidderCode: 'validalternatebidder', + adapterCode: 'knownadapter1' + } + ); + logWarnSpy = sinon.spy(utils, 'logWarn'); + bidderSettingStub = sinon.stub(bidderSettings, 'get'); + aliasRegistry = {}; + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); + }); - it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { - bidderSettingStub.returns(false); + afterEach(function () { + logWarnSpy.restore(); + bidderSettingStub.restore(); + aliasRegistryStub.restore(); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { + bidderSettingStub.returns(false); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { - bidderSettingStub.returns(undefined); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { + bidderSettingStub.returns(undefined); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); - expect(addBidResponseStub.called).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - aliasRegistry = {'validAlternateBidder': CODE}; + expect(addBidResponseStub.called).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + aliasRegistry = {'validAlternateBidder': CODE}; - expect(addBidResponseStub.called).to.equal(false); - expect(logWarnSpy.callCount).to.equal(1); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.called).to.equal(false); + expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); }); - }); - describe('when interpretResponse returns BidderAuctionResponse', function() { - const bidRequest = { - bids: [{ + describe('when interpretResponse returns BidderAuctionResponse', function() { + const bidRequest = { + auctionId: 'aid', + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'aid', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + const paapiConfig = { bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - config: { - foo: 'bar' - } - } - describe('when response has FLEDGE auction config', function() { - let fledgeStub; - - function fledgeHook(next, ...args) { - fledgeStub(...args); + config: { + foo: 'bar' + } } - before(() => { - addComponentAuction.before(fledgeHook); - }); - - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) - - beforeEach(function () { - fledgeStub = sinon.stub(); - }); - it('should unwrap bids', function() { const bidder = newBidder(spec); spec.interpretResponse.returns({ bids: bids, - fledgeAuctionConfigs: [] }); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + sinon.assert.calledWith(addBidResponseStub, 'mock/placement', sinon.match(bids[0])); }); - it('should call fledgeManager with FLEDGE configs', function() { + it('does not unwrap bids from a bid that happens to have a "bids" property', () => { const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [fledgeAuctionConfig] - }); + const bid = Object.assign({ + bids: ['a', 'b'] + }, bids[0]); + spec.interpretResponse.returns(bid); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(fledgeStub.calledOnce).to.equal(true); - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + sinon.assert.calledWith(addBidResponseStub, 'mock/placement', sinon.match(bid)); }) - it('should call fledgeManager with FLEDGE configs even if no bids returned', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: [], - fledgeAuctionConfigs: [fledgeAuctionConfig] + describe('when response has PAAPI auction config', function() { + let paapiStub; + + function paapiHook(next, ...args) { + paapiStub(...args); + } + + before(() => { + addComponentAuction.before(paapiHook); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(fledgeStub.calledOnce).to.be.true; - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(false); + after(() => { + addComponentAuction.getHooks({hook: paapiHook}).remove(); + }) + + beforeEach(function () { + paapiStub = sinon.stub(); + }); + + const PAAPI_PROPS = ['fledgeAuctionConfigs', 'paapiAuctionConfigs']; + + it(`should not accept both ${PAAPI_PROPS.join(' and ')}`, () => { + const bidder = newBidder(spec); + spec.interpretResponse.returns(Object.fromEntries(PAAPI_PROPS.map(prop => [prop, [paapiConfig]]))) + expect(() => { + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + }).to.throw; + }) + + PAAPI_PROPS.forEach(paapiProp => { + describe(`using ${paapiProp}`, () => { + it('should call paapi hook with PAAPI configs', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + [paapiProp]: [paapiConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(paapiStub.calledOnce).to.equal(true); + sinon.assert.calledWith(paapiStub, bidRequest.bids[0], paapiConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + }) + + Object.entries({ + 'missing': undefined, + 'an empty array': [] + }).forEach(([t, bids]) => { + it(`should call paapi hook with PAAPI configs even when bids is ${t}`, function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids, + [paapiProp]: [paapiConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(paapiStub.calledOnce).to.be.true; + sinon.assert.calledWith(paapiStub, bidRequest.bids[0], paapiConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(false); + }) + }) + }) + }) }) }) - }) -}); + }); -describe('bid response isValid', () => { - describe('size check', () => { - let req, index; + describe('bid response isValid', () => { + describe('size check', () => { + let req, index; - beforeEach(() => { - req = { - ...MOCK_BIDS_REQUEST.bids[0], - mediaTypes: { - banner: { - sizes: [[1, 2], [3, 4]] + beforeEach(() => { + req = { + ...MOCK_BIDS_REQUEST.bids[0], + mediaTypes: { + banner: { + sizes: [[1, 2], [3, 4]] + } } } - } - }); + }); - function mkResponse(width, height) { - return { - requestId: req.bidId, - width, - height, - cpm: 1, - ttl: 60, - creativeId: '123', - netRevenue: true, - currency: 'USD', - mediaType: 'banner', + function mkResponse(width, height) { + return { + requestId: req.bidId, + width, + height, + cpm: 1, + ttl: 60, + creativeId: '123', + netRevenue: true, + currency: 'USD', + mediaType: 'banner', + } } - } - function checkValid(bid) { - return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); - } + function checkValid(bid) { + return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); + } - it('should succeed when response has a size that was in request', () => { - expect(checkValid(mkResponse(3, 4))).to.be.true; - }); - }) -}); + it('should succeed when response has a size that was in request', () => { + expect(checkValid(mkResponse(3, 4))).to.be.true; + }); + }) + }); +}) diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 98b317e0d36..1bcad3216ce 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -1,4 +1,4 @@ -import {ConsentHandler, gvlidRegistry} from '../../../../src/consentHandler.js'; +import {ConsentHandler, gvlidRegistry, multiHandler} from '../../../../src/consentHandler.js'; describe('Consent data handler', () => { let handler; @@ -56,6 +56,88 @@ describe('Consent data handler', () => { }) }) }); + + describe('getHash', () => { + it('is defined when null', () => { + expect(handler.hash).be.a('string'); + }); + it('changes when a field is updated', () => { + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: false}); + const h2 = handler.hash; + expect(h2).to.not.eql(h1); + handler.setConsentData({field: 'value', enabled: true}); + const h3 = handler.hash; + expect(h3).to.not.eql(h2); + expect(h3).to.not.eql(h1); + }); + it('does not change when fields are unchanged', () => { + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true}); + expect(handler.hash).to.eql(h1); + }); + it('does not change when non-hashFields are updated', () => { + handler.hashFields = ['field', 'enabled']; + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true, other: 'data'}); + expect(handler.hash).to.eql(h1); + }) + }) +}); + +describe('multiHandler', () => { + let handlers, multi; + beforeEach(() => { + handlers = {h1: {}, h2: {}}; + multi = multiHandler(handlers); + }); + + ['getConsentData', 'getConsentMeta'].forEach(method => { + describe(method, () => { + it('combines results from underlying handlers', () => { + handlers.h1[method] = () => 'one'; + handlers.h2[method] = () => 'two'; + expect(multi[method]()).to.eql({ + h1: 'one', + h2: 'two', + }) + }); + }); + }); + + describe('.promise', () => { + it('resolves all underlying promises', (done) => { + handlers.h1.promise = Promise.resolve('one'); + let resolver, result; + handlers.h2.promise = new Promise((resolve) => { resolver = resolve }); + multi.promise.then((val) => { + result = val; + expect(result).to.eql({ + h1: 'one', + h2: 'two' + }); + done(); + }) + handlers.h1.promise.then(() => { + expect(result).to.not.exist; + resolver('two'); + }); + }) + }); + + describe('.hash', () => { + ['h1', 'h2'].forEach((handler, i) => { + it(`changes when handler #${i + 1} changes hash`, () => { + handlers.h1.hash = 'one'; + handlers.h2.hash = 'two' + const first = multi.hash; + handlers[handler].hash = 'new'; + expect(multi.hash).to.not.eql(first); + }) + }) + }) }) describe('gvlidRegistry', () => { diff --git a/test/spec/unit/core/events_spec.js b/test/spec/unit/core/events_spec.js new file mode 100644 index 00000000000..e1451f657b5 --- /dev/null +++ b/test/spec/unit/core/events_spec.js @@ -0,0 +1,45 @@ +import {config} from 'src/config.js'; +import {emit, clearEvents, getEvents, on, off} from '../../../../src/events.js'; +import * as utils from '../../../../src/utils.js' + +describe('events', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + clearEvents(); + }); + afterEach(() => { + clock.restore(); + }); + + it('should clear event log using eventHistoryTTL config', () => { + emit('testEvent', {}); + expect(getEvents().length).to.eql(1); + config.setConfig({eventHistoryTTL: 1}); + clock.tick(500); + expect(getEvents().length).to.eql(1); + clock.tick(6000); + expect(getEvents().length).to.eql(0); + }); + + it('should take history TTL in seconds', () => { + emit('testEvent', {}); + config.setConfig({eventHistoryTTL: 1000}); + clock.tick(10000); + expect(getEvents().length).to.eql(1); + }); + + it('should include the eventString if a callback fails', () => { + const logErrorStub = sinon.stub(utils, 'logError'); + const eventString = 'bidWon'; + let fn = function() { throw new Error('Test error'); }; + on(eventString, fn); + + emit(eventString, {}); + + sinon.assert.calledWith(logErrorStub, 'Error executing handler:', 'events.js', sinon.match.instanceOf(Error), eventString); + + off(eventString, fn); + logErrorStub.restore(); + }); +}) diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index f16d6208087..ba9aeff70d1 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -1,13 +1,19 @@ -import { expect } from 'chai'; -import { targeting as targetingInstance, filters, getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm } from 'src/targeting.js'; -import { config } from 'src/config.js'; -import { createBidReceived } from 'test/fixtures/fixtures.js'; +import {expect} from 'chai'; +import { + filters, + getHighestCpmBidsFromBidPool, + sortByDealAndPriceBucketOrCpm, + targeting as targetingInstance +} from 'src/targeting.js'; +import {config} from 'src/config.js'; +import {createBidReceived} from 'test/fixtures/fixtures.js'; import CONSTANTS from 'src/constants.json'; -import { auctionManager } from 'src/auctionManager.js'; +import {auctionManager} from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; import {deepClone} from 'src/utils.js'; import {createBid} from '../../../../src/bidfactory.js'; import {hook} from '../../../../src/hook.js'; +import {getHighestCpm} from '../../../../src/utils/reducers.js'; function mkBid(bid, status = CONSTANTS.STATUS.GOOD) { return Object.assign(createBid(status), bid); @@ -451,7 +457,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -467,7 +473,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -949,6 +955,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); useBidCache = false; @@ -956,6 +963,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); }); it('should use bidCacheFilterFunction', function() { @@ -983,9 +991,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching Off, No Filter Function useBidCache = false; @@ -994,9 +1006,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On AGAIN, No Filter Function (should be same as first time) useBidCache = true; @@ -1005,9 +1021,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On, with Filter Function to Exclude video useBidCache = true; @@ -1020,9 +1040,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should have been called for each cached bid (4 times) expect(bcffCalled).to.equal(4); @@ -1038,9 +1062,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should not have been called expect(bcffCalled).to.equal(0); }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 5c361d186c0..7f55a2cddf0 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -14,7 +14,7 @@ import { config as configObj } from 'src/config.js'; import * as ajaxLib from 'src/ajax.js'; import * as auctionModule from 'src/auction.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; -import { _sendAdToCreative } from 'src/secureCreatives.js'; +import {resizeRemoteCreative} from 'src/secureCreatives.js'; import {find} from 'src/polyfill.js'; import * as pbjsModule from 'src/prebid.js'; import {hook} from '../../../src/hook.js'; @@ -25,6 +25,8 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {createBid} from '../../../src/bidfactory.js'; import {enrichFPD} from '../../../src/fpd/enrichment.js'; import {mockFpdEnrichments} from '../../helpers/fpd.js'; +import {generateUUID} from '../../../src/utils.js'; +import {getCreativeRenderer} from '../../../src/creativeRenderers.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -43,13 +45,13 @@ var adUnits = getAdUnits(); var adUnitCodes = getAdUnits().map(unit => unit.code); var bidsBackHandler = function() {}; const timeout = 2000; -var auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout}); -auction.getBidRequests = getBidRequests; -auction.getBidsReceived = getBidResponses; -auction.getAdUnits = getAdUnits; -auction.getAuctionStatus = function() { return auctionModule.AUCTION_COMPLETED } +const auctionId = generateUUID(); +let auction; function resetAuction() { + if (auction == null) { + auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout, labels: undefined, auctionId: auctionId}); + } $$PREBID_GLOBAL$$.setConfig({ enableSendAllBids: false }); auction.getBidRequests = getBidRequests; auction.getBidsReceived = getBidResponses; @@ -200,11 +202,13 @@ window.apntag = { describe('Unit: Prebid Module', function () { let bidExpiryStub, sandbox; - before(() => { + before((done) => { hook.ready(); $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); resetDebugging(); sinon.stub(filters, 'isActualBid').returns(true); // stub this out so that we can use vanilla objects as bids + // preload creative renderer + getCreativeRenderer({}).then(() => done()); }); beforeEach(function () { @@ -556,6 +560,7 @@ describe('Unit: Prebid Module', function () { 'bidderRequestId': '331f3cf3f1d9c8', 'auctionId': '20882439e3238c', 'transactionId': 'trdiv-gpt-ad-1460505748561-0', + 'adUnitId': 'audiv-gpt-ad-1460505748561-0', } ], 'auctionStart': 1505250713622, @@ -573,6 +578,7 @@ describe('Unit: Prebid Module', function () { let auctionManagerInstance = newAuctionManager(); targeting = newTargeting(auctionManagerInstance); let adUnits = [{ + adUnitId: 'audiv-gpt-ad-1460505748561-0', transactionId: 'trdiv-gpt-ad-1460505748561-0', code: 'div-gpt-ad-1460505748561-0', sizes: [[300, 250], [300, 600]], @@ -718,6 +724,7 @@ describe('Unit: Prebid Module', function () { const adUnit = { transactionId: `tr${code}`, + adUnitId: `au${code}`, code: code, sizes: [[300, 250], [300, 600]], bids: [{ @@ -820,6 +827,7 @@ describe('Unit: Prebid Module', function () { }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': 'trdiv-gpt-ad-1460505748561-0', + 'adUnitId': 'audiv-gpt-ad-1460505748561-0', 'sizes': [ [ 300, @@ -1082,35 +1090,6 @@ describe('Unit: Prebid Module', function () { expect(slots[0].spySetTargeting.args).to.deep.contain.members(expected); }); - it('should find correct gpt slot based on ad id rather than ad unit code when resizing secure creative', function () { - var slots = [ - new Slot('div-not-matching-adunit-code-1', config.adUnitCodes[0]), - new Slot('div-not-matching-adunit-code-2', config.adUnitCodes[0]), - new Slot('div-not-matching-adunit-code-3', config.adUnitCodes[0]) - ]; - - slots[1].setTargeting('hb_adid', ['someAdId']); - slots[1].spyGetSlotElementId.resetHistory(); - window.googletag.pubads().setSlots(slots); - - const mockAdObject = { - adId: 'someAdId', - ad: '', - adUrl: 'http://creative.prebid.org/${AUCTION_PRICE}', - width: 300, - height: 250, - renderer: null, - cpm: '1.00', - adUnitCode: config.adUnitCodes[0], - }; - - _sendAdToCreative(mockAdObject, sinon.stub()); - - expect(slots[0].spyGetSlotElementId.called).to.equal(false); - expect(slots[1].spyGetSlotElementId.called).to.equal(true); - expect(slots[2].spyGetSlotElementId.called).to.equal(false); - }); - it('Calling enableSendAllBids should set targeting to include standard keys with bidder' + ' append to key name', function () { var slots = createSlotArray(); @@ -1232,9 +1211,14 @@ describe('Unit: Prebid Module', function () { height: 0 } }, + body: { + appendChild: sinon.stub() + }, getElementsByTagName: sinon.stub(), - querySelector: sinon.stub() + querySelector: sinon.stub(), + createElement: sinon.stub(), }; + doc.defaultView.document = doc; elStub = { insertBefore: sinon.stub() @@ -1264,7 +1248,7 @@ describe('Unit: Prebid Module', function () { it('should require doc and id params', function () { $$PREBID_GLOBAL$$.renderAd(); - var error = 'Error trying to write ad Id :undefined to the page. Missing adId'; + var error = 'Error rendering ad (id: undefined): missing adId'; assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); }); @@ -1289,14 +1273,13 @@ describe('Unit: Prebid Module', function () { adUrl: 'http://server.example.com/ad/ad.js' }); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.ok(elStub.insertBefore.called, 'url was written to iframe in doc'); + sinon.assert.calledWith(doc.createElement, 'iframe'); }); it('should log an error when no ad or url', function () { pushBidResponseToAuction({}); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var error = 'Error trying to write ad. No ad for bid response id: ' + bidId; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + sinon.assert.called(spyLogError); }); it('should log an error when not in an iFrame', function () { @@ -1305,7 +1288,7 @@ describe('Unit: Prebid Module', function () { }); inIframe = false; $$PREBID_GLOBAL$$.renderAd(document, bidId); - const error = 'Error trying to write ad. Ad render call ad id ' + bidId + ' was prevented from writing to the main document.'; + const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; assert.ok(spyLogError.calledWith(error), 'expected error was logged'); }); @@ -1326,14 +1309,14 @@ describe('Unit: Prebid Module', function () { doc.write = sinon.stub().throws(error); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var errorMessage = 'Error trying to write ad Id :' + bidId + ' to the page:' + error.message; + var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); }); it('should log an error when ad not found', function () { var fakeId = 99; $$PREBID_GLOBAL$$.renderAd(doc, fakeId); - var error = 'Error trying to write ad. Cannot find ad by given id : ' + fakeId; + var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` assert.ok(spyLogError.calledWith(error), 'expected error was logged'); }); @@ -1345,14 +1328,6 @@ 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({ @@ -1540,6 +1515,7 @@ describe('Unit: Prebid Module', function () { } }, transactionId: 'mock-tid', + adUnitId: 'mock-au', bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ] @@ -1614,6 +1590,7 @@ describe('Unit: Prebid Module', function () { height: 250, adUnitCode: bidRequests[0].bids[0].adUnitCode, transactionId: 'mock-tid', + adUnitId: 'mock-au', adserverTargeting: { 'hb_bidder': BIDDER_CODE, 'hb_adid': bidId, @@ -1692,7 +1669,8 @@ describe('Unit: Prebid Module', function () { const bid = { bidder: 'mock-bidder', adUnitCode: adUnits[0].code, - transactionId: adUnits[0].transactionId + transactionId: adUnits[0].transactionId, + adUnitId: adUnits[0].adUnitId, } requestBids({ adUnits, @@ -1978,6 +1956,102 @@ describe('Unit: Prebid Module', function () { .and.to.match(/[a-f0-9\-]{36}/i); }); + it('should use the same transactionID for ad units with the same code', () => { + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [ + { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [] + }, { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [] + } + ] + }); + const tid = auctionArgs.adUnits[0].transactionId; + expect(tid).to.exist; + expect(auctionArgs.adUnits[1].transactionId).to.eql(tid); + }); + + it('should re-use pub-provided transaction ID for ad units with the same code', () => { + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [ + { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [], + }, { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [], + ortb2Imp: { + ext: { + tid: 'pub-tid' + } + } + } + ] + }); + expect(auctionArgs.adUnits.map(au => au.transactionId)).to.eql(['pub-tid', 'pub-tid']); + }); + + it('should use pub-provided TIDs when they conflict for ad units with the same code', () => { + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [ + { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [], + ortb2Imp: { + ext: { + tid: 't1' + } + } + }, { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [], + ortb2Imp: { + ext: { + tid: 't2' + } + } + } + ] + }); + expect(auctionArgs.adUnits.map(au => au.transactionId)).to.eql(['t1', 't2']); + }); + + it('should generate unique adUnitId', () => { + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [ + { + code: 'single', + mediaTypes: { banner: { sizes: [] } }, + bids: [] + }, { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [] + }, + { + code: 'twin', + mediaTypes: { banner: { sizes: [] } }, + bids: [] + } + ] + }); + + const ids = new Set(); + auctionArgs.adUnits.forEach(au => { + expect(au.adUnitId).to.exist; + ids.add(au.adUnitId); + }); + expect(ids.size).to.eql(3); + }); + describe('transactionId', () => { let adUnit; beforeEach(() => { @@ -2736,6 +2810,13 @@ describe('Unit: Prebid Module', function () { events.on.restore(); }); + it('should emit event BID_ACCEPTED when invoked', function () { + var callback = sinon.spy(); + $$PREBID_GLOBAL$$.onEvent('bidAccepted', callback); + events.emit(CONSTANTS.EVENTS.BID_ACCEPTED); + sinon.assert.calledOnce(callback); + }); + describe('beforeRequestBids', function () { let bidRequestedHandler; let beforeRequestBidsHandler; @@ -3304,16 +3385,20 @@ describe('Unit: Prebid Module', function () { const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); expect(highestBid).to.deep.equal(_bidsReceived[2]) }) - }) + }); - describe('getHighestCpm', () => { + describe('getHighestCpmBids', () => { after(() => { resetAuction(); }); it('returns an array containing the highest bid object for the given adUnitCode', function () { - const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids('/19968336/header-bid-tag-0'); + const adUnitcode = '/19968336/header-bid-tag-0'; + targeting.setLatestAuctionForAdUnit(adUnitcode, auctionId) + const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids(adUnitcode); expect(highestCpmBids.length).to.equal(1); - expect(highestCpmBids[0]).to.deep.equal(auctionManager.getBidsReceived()[1]); + const expectedBid = auctionManager.getBidsReceived()[1]; + expectedBid.latestTargetedAuctionId = auctionId; + expect(highestCpmBids[0]).to.deep.equal(expectedBid); }); it('returns an empty array when the given adUnit is not found', function () { @@ -3543,7 +3628,7 @@ describe('Unit: Prebid Module', function () { { code: 'adUnit-code-1', mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, - transactionId: '1234567890', + adUnitId: '1234567890', bids: [ { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1' } ] @@ -3552,15 +3637,15 @@ describe('Unit: Prebid Module', function () { code: 'adUnit-code-2', deferBilling: true, mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, - transactionId: '0987654321', + adUnitId: '0987654321', bids: [ { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2' } ] } ]; - let winningBid1 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1', transactionId: '1234567890', adId: 'abcdefg' } - let winningBid2 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2', transactionId: '0987654321' } + let winningBid1 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1', adUnitId: '1234567890', adId: 'abcdefg' } + let winningBid2 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2', adUnitId: '0987654321' } let adUnitCodes = ['adUnit-code-1', 'adUnit-code-2']; let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: 2000}); diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 7d5f9af35dd..a7be4e327f0 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,6 +1,4 @@ -import { - _sendAdToCreative, getReplier, receiveMessage -} from 'src/secureCreatives.js'; +import {getReplier, receiveMessage, resizeRemoteCreative} from 'src/secureCreatives.js'; import * as utils from 'src/utils.js'; import {getAdUnits, getBidRequests, getBidResponses} from 'test/fixtures/fixtures.js'; import {auctionManager} from 'src/auctionManager.js'; @@ -8,14 +6,26 @@ import * as auctionModule from 'src/auction.js'; import * as native from 'src/native.js'; import {fireNativeTrackers, getAllAssetsMessage} from 'src/native.js'; import * as events from 'src/events.js'; -import { config as configObj } from 'src/config.js'; +import {config as configObj} from 'src/config.js'; +import * as creativeRenderers from 'src/creativeRenderers.js'; import 'src/prebid.js'; +import 'modules/nativeRendering.js'; -import { expect } from 'chai'; +import {expect} from 'chai'; var CONSTANTS = require('src/constants.json'); describe('secureCreatives', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + function makeEvent(ev) { return Object.assign({origin: 'mock-origin', ports: []}, ev) } @@ -54,40 +64,6 @@ describe('secureCreatives', () => { }); }); - describe('_sendAdToCreative', () => { - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); - }); - it('should macro replace ${AUCTION_PRICE} with the winning bid for ad and adUrl', () => { - const oldVal = window.googletag; - const oldapntag = window.apntag; - window.apntag = null - window.googletag = null; - const mockAdObject = { - adId: 'someAdId', - ad: '', - adUrl: 'http://creative.prebid.org/${AUCTION_PRICE}', - width: 300, - height: 250, - renderer: null, - cpm: '1.00', - adUnitCode: 'some_dom_id' - }; - const reply = sinon.spy(); - _sendAdToCreative(mockAdObject, reply); - expect(reply.args[0][0].ad).to.equal(''); - expect(reply.args[0][0].adUrl).to.equal('http://creative.prebid.org/1.00'); - window.googletag = oldVal; - window.apntag = oldapntag; - }); - }); - describe('receiveMessage', function() { const bidId = 1; const warning = `Ad id ${bidId} has been rendered before`; @@ -149,19 +125,15 @@ describe('secureCreatives', () => { }); beforeEach(function() { - spyAddWinningBid = sinon.spy(auctionManager, 'addWinningBid'); - spyLogWarn = sinon.spy(utils, 'logWarn'); - stubFireNativeTrackers = sinon.stub(native, 'fireNativeTrackers').callsFake(message => { return message.action; }); - stubGetAllAssetsMessage = sinon.stub(native, 'getAllAssetsMessage'); - stubEmit = sinon.stub(events, 'emit'); + spyAddWinningBid = sandbox.spy(auctionManager, 'addWinningBid'); + spyLogWarn = sandbox.spy(utils, 'logWarn'); + stubFireNativeTrackers = sandbox.stub(native, 'fireNativeTrackers').callsFake(message => { return message.action; }); + stubGetAllAssetsMessage = sandbox.stub(native, 'getAllAssetsMessage'); + stubEmit = sandbox.stub(events, 'emit'); }); afterEach(function() { - spyAddWinningBid.restore(); - spyLogWarn.restore(); - stubFireNativeTrackers.restore(); - stubGetAllAssetsMessage.restore(); - stubEmit.restore(); + sandbox.restore(); resetAuction(); adResponse.adId = bidId; }); @@ -305,6 +277,66 @@ describe('secureCreatives', () => { adId: bidId })); }); + + it('should include renderers in responses', () => { + sandbox.stub(creativeRenderers, 'getCreativeRendererSource').returns('mock-renderer'); + pushBidResponseToAuction({}); + const ev = makeEvent({ + source: { + postMessage: sinon.stub() + }, + data: JSON.stringify({adId: bidId, message: 'Prebid Request'}) + }); + receiveMessage(ev); + sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => JSON.parse(ob).renderer === 'mock-renderer')); + }); + + if (FEATURES.NATIVE) { + it('should include native rendering data in responses', () => { + const bid = { + native: { + ortb: { + assets: [ + { + id: 1, + data: { + type: 2, + value: 'vbody' + } + } + ] + }, + body: 'vbody', + adTemplate: 'tpl', + rendererUrl: 'rurl' + } + } + pushBidResponseToAuction(bid); + const ev = makeEvent({ + source: { + postMessage: sinon.stub() + }, + data: JSON.stringify({adId: bidId, message: 'Prebid Request'}) + }) + receiveMessage(ev); + sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => { + const data = JSON.parse(ob); + ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist); + const native = data.native; + sinon.assert.match(native, { + ortb: bid.native.ortb, + adTemplate: bid.native.adTemplate, + rendererUrl: bid.native.rendererUrl, + }) + expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({ + adTemplate: bid.native.adTemplate, + rendererUrl: bid.native.rendererUrl, + body: 'vbody' + }); + return true; + })) + }) + } }); describe('Prebid Native', function() { @@ -365,45 +397,6 @@ describe('secureCreatives', () => { receiveMessage(ev); stubEmit.withArgs(CONSTANTS.EVENTS.BID_WON, adResponse).calledOnce; }); - - it('Prebid native should fire trackers', function () { - let adId = 2; - pushBidResponseToAuction({adId}); - - const data = { - adId: adId, - message: 'Prebid Native', - action: 'click', - }; - - const ev = makeEvent({ - data: JSON.stringify(data), - source: { - postMessage: sinon.stub() - }, - origin: 'any origin' - }); - - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubFireNativeTrackers); - sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.BID_WON, adResponse); - sinon.assert.calledOnce(spyAddWinningBid); - - resetHistories(ev.source.postMessage); - - delete data.action; - ev.data = JSON.stringify(data); - receiveMessage(ev); - - sinon.assert.neverCalledWith(spyLogWarn, warning); - sinon.assert.calledOnce(stubFireNativeTrackers); - sinon.assert.neverCalledWith(stubEmit, CONSTANTS.EVENTS.BID_WON); - sinon.assert.notCalled(spyAddWinningBid); - - expect(adResponse).to.have.property('status', CONSTANTS.BID_STATUS.RENDERED); - }); }); describe('Prebid Event', () => { @@ -457,4 +450,53 @@ describe('secureCreatives', () => { }); }); }); + + describe('resizeRemoteCreative', () => { + let origGpt; + before(() => { + origGpt = window.googletag; + }); + after(() => { + window.googletag = origGpt; + }); + function mockSlot(elementId, pathId) { + let targeting = {}; + return { + getSlotElementId: sinon.stub().callsFake(() => elementId), + getAdUnitPath: sinon.stub().callsFake(() => pathId), + setTargeting: sinon.stub().callsFake((key, value) => { + value = Array.isArray(value) ? value : [value]; + targeting[key] = value; + }), + getTargetingKeys: sinon.stub().callsFake(() => Object.keys(targeting)), + getTargeting: sinon.stub().callsFake((key) => targeting[key] || []) + } + } + let slots; + beforeEach(() => { + slots = [ + mockSlot('div1', 'au1'), + mockSlot('div2', 'au2'), + mockSlot('div3', 'au3') + ] + window.googletag = { + pubads: sinon.stub().returns({ + getSlots: sinon.stub().returns(slots) + }) + }; + sandbox.stub(document, 'getElementById'); + }) + + it('should find correct gpt slot based on ad id rather than ad unit code when resizing secure creative', function () { + slots[1].setTargeting('hb_adid', ['adId']); + resizeRemoteCreative({ + adId: 'adId', + width: 300, + height: 250, + }); + [0, 2].forEach((i) => sinon.assert.notCalled(slots[i].getSlotElementId)) + sinon.assert.called(slots[1].getSlotElementId); + sinon.assert.calledWith(document.getElementById, 'div2'); + }); + }) }); diff --git a/test/spec/unit/utils/reducers_spec.js b/test/spec/unit/utils/reducers_spec.js new file mode 100644 index 00000000000..95bf3b74041 --- /dev/null +++ b/test/spec/unit/utils/reducers_spec.js @@ -0,0 +1,124 @@ +import { + tiebreakCompare, + keyCompare, + simpleCompare, + minimum, + maximum, + getHighestCpm, + getOldestHighestCpmBid, getLatestHighestCpmBid, reverseCompare +} from '../../../../src/utils/reducers.js'; +import assert from 'assert'; + +describe('reducers', () => { + describe('simpleCompare', () => { + Object.entries({ + '<': [10, 20, -1], + '===': [123, 123, 0], + '>': [30, -10, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when a ${t} b`, () => { + expect(simpleCompare(a, b)).to.equal(expected); + }) + }) + }); + + describe('keyCompare', () => { + Object.entries({ + '<': [{k: -123}, {k: 0}, -1], + '===': [{k: 0}, {k: 0}, 0], + '>': [{k: 2}, {k: 1}, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when key(a) ${t} key(b)`, () => { + expect(keyCompare(item => item.k)(a, b)).to.equal(expected); + }) + }) + }); + + describe('tiebreakCompare', () => { + Object.entries({ + 'first compare says a < b': [{main: 1, tie: 2}, {main: 2, tie: 1}, -1], + 'first compare says a > b': [{main: 2, tie: 1}, {main: 1, tie: 2}, 1], + 'first compare ties, second says a < b': [{main: 0, tie: 1}, {main: 0, tie: 2}, -1], + 'first compare ties, second says a > b': [{main: 0, tie: 2}, {main: 0, tie: 1}, 1], + 'all compares tie': [{main: 0, tie: 0}, {main: 0, tie: 0}, 0] + }).forEach(([t, [a, b, expected]]) => { + it(`should return ${expected} when ${t}`, () => { + const cmp = tiebreakCompare(keyCompare(item => item.main), keyCompare(item => item.tie)); + expect(cmp(a, b)).to.equal(expected); + }) + }) + }); + + const SAMPLE_ARR = [-10, 20, 20, 123, 400]; + + Object.entries({ + 'minimum': [minimum, ['minimum', -10], ['maximum', 400]], + 'maximum': [maximum, ['maximum', 400], ['minimum', -10]] + }).forEach(([t, [fn, simple, reversed]]) => { + describe(t, () => { + it(`should find ${simple[0]} using simple compare`, () => { + expect(SAMPLE_ARR.reduce(fn(simpleCompare))).to.equal(simple[1]); + }); + it(`should find ${reversed[0]} using reverse compare`, () => { + expect(SAMPLE_ARR.reduce(fn(reverseCompare()))).to.equal(reversed[1]); + }); + }) + }); + + describe('getHighestCpm', function () { + it('should pick the highest cpm', function () { + let a = { + cpm: 2, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 100 + }; + expect(getHighestCpm(a, b)).to.eql(a); + expect(getHighestCpm(b, a)).to.eql(a); + }); + + it('should pick the lowest timeToRespond cpm in case of tie', function () { + let a = { + cpm: 1, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 50 + }; + expect(getHighestCpm(a, b)).to.eql(b); + expect(getHighestCpm(b, a)).to.eql(b); + }); + }); + + describe('getOldestHighestCpmBid', () => { + it('should pick the oldest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getOldestHighestCpmBid(a, b)).to.eql(a); + expect(getOldestHighestCpmBid(b, a)).to.eql(a); + }); + }); + describe('getLatestHighestCpmBid', () => { + it('should pick the latest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getLatestHighestCpmBid(a, b)).to.eql(b); + expect(getLatestHighestCpmBid(b, a)).to.eql(b); + }); + }); +}) diff --git a/test/spec/unit/utils/ttlCollection_spec.js b/test/spec/unit/utils/ttlCollection_spec.js new file mode 100644 index 00000000000..76cfa32d955 --- /dev/null +++ b/test/spec/unit/utils/ttlCollection_spec.js @@ -0,0 +1,207 @@ +import {ttlCollection} from '../../../../src/utils/ttlCollection.js'; + +describe('ttlCollection', () => { + it('can add & retrieve items', () => { + const coll = ttlCollection(); + expect(coll.toArray()).to.eql([]); + coll.add(1); + coll.add(2); + expect(coll.toArray()).to.eql([1, 2]); + }); + + it('can clear', () => { + const coll = ttlCollection(); + coll.add('item'); + coll.clear(); + expect(coll.toArray()).to.eql([]); + }); + + it('can be iterated over', () => { + const coll = ttlCollection(); + coll.add('1'); + coll.add('2'); + expect(Array.from(coll)).to.eql(['1', '2']); + }) + + describe('autopurge', () => { + let clock, pms, waitForPromises; + const SLACK = 2000; + beforeEach(() => { + clock = sinon.useFakeTimers(); + pms = []; + waitForPromises = () => Promise.all(pms); + }); + afterEach(() => { + clock.restore(); + }); + + Object.entries({ + 'defer': (value) => { + const pm = Promise.resolve(value); + pms.push(pm); + return pm; + }, + 'do not defer': (value) => value, + }).forEach(([t, resolve]) => { + describe(`when ttl/startTime ${t}`, () => { + let coll; + beforeEach(() => { + coll = ttlCollection({ + startTime: (item) => resolve(item.start == null ? new Date().getTime() : item.start), + ttl: (item) => resolve(item.ttl), + slack: SLACK + }) + }); + + it('should clear items after enough time has passed', () => { + coll.add({no: 'ttl'}); + coll.add({ttl: 1000}); + coll.add({ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 1000}, {ttl: 4000}]); + clock.tick(SLACK + 500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 4000}]); + clock.tick(3000); + expect(coll.toArray()).to.eql([{no: 'ttl'}]); + }); + }); + + it('should run onExpiry when items are cleared', () => { + const i1 = {ttl: 1000, some: 'data'}; + const i2 = {ttl: 2000, some: 'data'}; + coll.add(i1); + coll.add(i2); + const cb = sinon.stub(); + coll.onExpiry(cb); + return waitForPromises().then(() => { + clock.tick(500); + sinon.assert.notCalled(cb); + clock.tick(SLACK + 500); + sinon.assert.calledWith(cb, i1); + clock.tick(3000); + sinon.assert.calledWith(cb, i2); + }) + }); + + it('should allow unregistration of onExpiry callbacks', () => { + const cb = sinon.stub(); + coll.add({ttl: 500}); + coll.onExpiry(cb)(); + return waitForPromises().then(() => { + clock.tick(500 + SLACK); + sinon.assert.notCalled(cb); + }) + }) + + it('should not wait too long if a shorter ttl shows up', () => { + coll.add({ttl: 4000}); + coll.add({ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([ + {ttl: 4000} + ]); + }); + }); + + it('should not wait more if later ttls are within slack', () => { + coll.add({start: 0, ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(4000); + coll.add({start: 0, ttl: 5000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should clear items ASAP if they expire in the past', () => { + clock.tick(10000); + coll.add({start: 0, ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + it('should clear items ASAP if they have ttl = 0', () => { + coll.add({ttl: 0}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + describe('refresh', () => { + it('should refresh missing TTLs', () => { + const item = {}; + coll.add(item); + return waitForPromises().then(() => { + item.ttl = 1000; + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([item]); + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(1); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + }); + + it('should refresh existing TTLs', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000); + item.ttl = 4000; + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([item]); + clock.tick(3000); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should discard initial TTL if it does not resolve before a refresh', () => { + let resolveTTL; + const item = { + ttl: new Promise((resolve) => { + resolveTTL = resolve; + }) + }; + coll.add(item); + item.ttl = null; + coll.refresh(); + resolveTTL(1000); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + + it('should discard TTLs on clear', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + coll.clear(); + item.ttl = null; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index e26683074c8..c84fe124db6 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,7 +2,9 @@ import {getAdServerTargeting} from 'test/fixtures/fixtures.js'; import {expect} from 'chai'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; -import {deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js'; +import {binarySearch, deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js'; var assert = require('assert'); @@ -39,28 +41,6 @@ describe('Utils', function () { }); }); - describe('tryAppendQueryString', function () { - it('should append query string to existing url', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = 'c'; - - var output = utils.tryAppendQueryString(url, key, value); - - var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; - assert.equal(output, expectedResult); - }); - - it('should return existing url, if the value is empty', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = ''; - - var output = utils.tryAppendQueryString(url, key, value); - assert.equal(output, url); - }); - }); - describe('parseQueryStringParameters', function () { it('should append query string to existing using the input obj', function () { var obj = { @@ -538,72 +518,6 @@ describe('Utils', function () { }); }); - describe('getHighestCpm', function () { - it('should pick the existing highest cpm', function () { - let previous = { - cpm: 2, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), previous); - }); - - it('should pick the new highest cpm', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 2, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the fastest cpm in case of tie', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 50 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the oldest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getOldestHighestCpmBid(previous, current), previous); - }); - - it('should pick the latest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getLatestHighestCpmBid(previous, current), current); - }); - }); - describe('polyfill test', function () { it('should not add polyfill to array', function() { var arr = ['hello', 'world']; @@ -765,43 +679,15 @@ describe('Utils', function () { describe('convertCamelToUnderscore', function () { it('returns converted string value using underscore syntax instead of camelCase', function () { let var1 = 'placementIdTest'; - let test1 = utils.convertCamelToUnderscore(var1); + let test1 = convertCamelToUnderscore(var1); expect(test1).to.equal('placement_id_test'); let var2 = 'my_test_value'; - let test2 = utils.convertCamelToUnderscore(var2); + let test2 = convertCamelToUnderscore(var2); expect(test2).to.equal(var2); }); }); - describe('getAdUnitSizes', function () { - it('returns an empty response when adUnits is undefined', function () { - let sizes = utils.getAdUnitSizes(); - expect(sizes).to.be.undefined; - }); - - it('returns an empty array when invalid data is present in adUnit object', function () { - let sizes = utils.getAdUnitSizes({ sizes: 300 }); - expect(sizes).to.deep.equal([]); - }); - - it('retuns an array of arrays when reading from adUnit.sizes', function () { - let sizes = utils.getAdUnitSizes({ sizes: [300, 250] }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ sizes: [[300, 250], [300, 600]] }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - - it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { - let sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [300, 250] } } }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } } }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - }); - describe('URL helpers', function () { describe('parseUrl()', function () { let parsed; @@ -1116,6 +1002,15 @@ describe('Utils', function () { const obj = {key: 'value'}; expect(deepEqual({outer: obj}, {outer: new Typed(obj)}, {checkTypes: true})).to.be.false; }); + it('should work when adding properties to the prototype of Array', () => { + after(function () { + // eslint-disable-next-line no-extend-native + delete Array.prototype.unitTestTempProp; + }); + // eslint-disable-next-line no-extend-native + Array.prototype.unitTestTempProp = 'testing'; + expect(deepEqual([], [])).to.be.true; + }); describe('cyrb53Hash', function() { it('should return the same hash for the same string', function() { @@ -1233,5 +1128,44 @@ describe('memoize', () => { mem('one', 'three'); expect(mem('one', 'three')).to.eql(['one', 'three']); expect(fn.callCount).to.eql(2); - }) + }); + + describe('binarySearch', () => { + [ + { + arr: [], + tests: [ + ['any', 0] + ] + }, + { + arr: [10], + tests: [ + [5, 0], + [10, 0], + [20, 1], + ], + }, + { + arr: [10, 20, 30, 30, 40], + tests: [ + [5, 0], + [15, 1], + [10, 0], + [30, 2], + [35, 4], + [40, 4], + [100, 5] + ] + } + ].forEach(({arr, tests}) => { + describe(`on ${arr}`, () => { + tests.forEach(([el, pos]) => { + it(`finds index for ${el} => ${pos}`, () => { + expect(binarySearch(arr, el)).to.equal(pos); + }); + }); + }); + }) + }); }) diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index c7c0b2eb329..fc6e71779cb 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -1,10 +1,10 @@ import chai from 'chai'; -import { getCacheUrl, store } from 'src/videoCache.js'; -import { config } from 'src/config.js'; -import { server } from 'test/mocks/xhr.js'; +import {getCacheUrl, store} from 'src/videoCache.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr.js'; import {auctionManager} from '../../src/auctionManager.js'; import {AuctionIndex} from '../../src/auctionIndex.js'; -import { batchingCache } from '../../src/auction.js'; +import {batchingCache} from '../../src/auction.js'; const should = chai.should(); @@ -127,7 +127,7 @@ describe('The video cache', function () { prebid.org wrapper - + @@ -149,6 +149,20 @@ describe('The video cache', function () { assertRequestMade({ vastUrl: 'my-mock-url.com', vastImpUrl: 'imptracker.com', ttl: 25 }, expectedValue) }); + it('should include multiple vastImpUrl when it\'s an array', function() { + const expectedValue = ` + + + prebid.org wrapper + + + + + + `; + assertRequestMade({ vastUrl: 'my-mock-url.com', vastImpUrl: ['https://vasttracking.mydomain.com/vast?cpm=1.2', 'imptracker.com'], ttl: 25, cpm: 1.2 }, expectedValue) + }); + it('should make the expected request when store() is called on an ad with vastXml', function () { const vastXml = ''; assertRequestMade({ vastXml: vastXml, ttl: 25 }, vastXml); @@ -174,7 +188,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -224,7 +238,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -295,7 +309,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -356,7 +370,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); JSON.parse(request.requestBody).should.deep.equal({ puts: [{ diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 61621c7ec42..3252c58c687 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,4 +1,4 @@ -import { isValidVideoBid } from 'src/video.js'; +import {fillVideoDefaults, isValidVideoBid} from 'src/video.js'; import {hook} from '../../src/hook.js'; import {stubAuctionIndex} from '../helpers/indexStub.js'; @@ -7,97 +7,169 @@ describe('video.js', function () { hook.ready(); }); - it('validates valid instream bids', function () { - const bid = { - adId: '456xyz', - vastUrl: 'http://www.example.com/vastUrl', - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + describe('fillVideoDefaults', () => { + function fillDefaults(videoMediaType = {}) { + const adUnit = {mediaTypes: {video: videoMediaType}}; + fillVideoDefaults(adUnit); + return adUnit.mediaTypes.video; + } - it('catches invalid instream bids', function () { - const bid = { - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + describe('should set plcmt = 4 when', () => { + it('context is "outstream"', () => { + expect(fillDefaults({context: 'outstream'})).to.eql({ + context: 'outstream', + plcmt: 4 + }) + }); + [2, 3, 4].forEach(placement => { + it(`placemement is "${placement}"`, () => { + expect(fillDefaults({placement})).to.eql({ + placement, + plcmt: 4 + }); + }) + }); + }); + describe('should set plcmt = 2 when', () => { + [2, 6].forEach(playbackmethod => { + it(`playbackmethod is "${playbackmethod}"`, () => { + expect(fillDefaults({playbackmethod})).to.eql({ + playbackmethod, + plcmt: 2, + }); + }); + }); + }); + describe('should not set plcmt when', () => { + Object.entries({ + 'it was set by pub (context=outstream)': { + expected: 1, + video: { + context: 'outstream', + plcmt: 1 + } + }, + 'it was set by pub (placement=2)': { + expected: 1, + video: { + placement: 2, + plcmt: 1 + } + }, + 'placement not in 2, 3, 4': { + expected: undefined, + video: { + placement: 1 + } + }, + 'it was set by pub (playbackmethod=2)': { + expected: 1, + video: { + plcmt: 1, + playbackmethod: 2 + } + } + }).forEach(([t, {expected, video}]) => { + it(t, () => { + expect(fillDefaults(video).plcmt).to.eql(expected); + }) + }) + }) + }) - it('catches invalid bids when prebid-cache is disabled', function () { - const adUnits = [{ - transactionId: 'au', - bidder: 'vastOnlyVideoBidder', - mediaTypes: {video: {}}, - }]; + describe('isValidVideoBid', () => { + it('validates valid instream bids', function () { + const bid = { + adId: '456xyz', + vastUrl: 'http://www.example.com/vastUrl', + adUnitId: 'au' + }; + const adUnits = [{ + adUnitId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); + it('catches invalid instream bids', function () { + const bid = { + adUnitId: 'au' + }; + const adUnits = [{ + adUnitId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); - expect(valid).to.equal(false); - }); + it('catches invalid bids when prebid-cache is disabled', function () { + const adUnits = [{ + adUnitId: 'au', + bidder: 'vastOnlyVideoBidder', + mediaTypes: {video: {}}, + }]; - it('validates valid outstream bids', function () { - const bid = { - transactionId: 'au', - renderer: { - url: 'render.url', - render: () => true, - } - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + const valid = isValidVideoBid({ adUnitId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); - it('validates valid outstream bids with a publisher defined renderer', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: { - context: 'outstream', + expect(valid).to.equal(false); + }); + + it('validates valid outstream bids', function () { + const bid = { + adUnitId: 'au', + renderer: { + url: 'render.url', + render: () => true, } - }, - renderer: { - url: 'render.url', - render: () => true, - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + }; + const adUnits = [{ + adUnitId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - it('catches invalid outstream bids', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + adUnitId: 'au', + }; + const adUnits = [{ + adUnitId: 'au', + mediaTypes: { + video: { + context: 'outstream', + } + }, + renderer: { + url: 'render.url', + render: () => true, + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); + + it('catches invalid outstream bids', function () { + const bid = { + adUnitId: 'au', + }; + const adUnits = [{ + adUnitId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); + }) }); diff --git a/test/test_deps.js b/test/test_deps.js index 35713106f8c..c8a3bcc9426 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -4,6 +4,16 @@ window.process = { } }; +window.addEventListener('error', function (ev) { + // eslint-disable-next-line no-console + console.error('Uncaught exception:', ev.error, ev.error?.stack); +}) + +window.addEventListener('unhandledrejection', function (ev) { + // eslint-disable-next-line no-console + console.error('Unhandled rejection:', ev.reason); +}) + require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); diff --git a/wdio.conf.js b/wdio.conf.js index 3d93f909971..d23fecd0b15 100644 --- a/wdio.conf.js +++ b/wdio.conf.js @@ -1,3 +1,5 @@ +const shared = require('./wdio.shared.conf.js'); + const browsers = Object.fromEntries( Object.entries(require('./browsers.json')) .filter(([k, v]) => { @@ -35,14 +37,7 @@ function getCapabilities() { } exports.config = { - specs: [ - './test/spec/e2e/**/*.spec.js', - ], - exclude: [ - // TODO: decipher original intent for "longform" tests - // they all appear to be almost exact copies - './test/spec/e2e/longform/**/*' - ], + ...shared.config, services: [ ['browserstack', { browserstackLocal: true @@ -53,17 +48,4 @@ exports.config = { maxInstances: 5, // Do not increase this, since we have only 5 parallel tests in browserstack account maxInstancesPerCapability: 1, capabilities: getCapabilities(), - logLevel: 'info', // put option here: info | trace | debug | warn| error | silent - bail: 0, - waitforTimeout: 60000, // Default timeout for all waitFor* commands. - connectionRetryTimeout: 60000, // Default timeout in milliseconds for request if Selenium Grid doesn't send response - connectionRetryCount: 3, // Default request retries count - framework: 'mocha', - mochaOpts: { - ui: 'bdd', - timeout: 60000, - compilers: ['js:babel-register'], - }, - // if you see error, update this to spec reporter and logLevel above to get detailed report. - reporters: ['spec'] } diff --git a/wdio.local.conf.js b/wdio.local.conf.js new file mode 100644 index 00000000000..772448472bf --- /dev/null +++ b/wdio.local.conf.js @@ -0,0 +1,13 @@ +const shared = require('./wdio.shared.conf.js'); + +exports.config = { + ...shared.config, + capabilities: [ + { + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['headless', 'disable-gpu'], + }, + }, + ], +}; diff --git a/wdio.shared.conf.js b/wdio.shared.conf.js new file mode 100644 index 00000000000..34e1ee9c675 --- /dev/null +++ b/wdio.shared.conf.js @@ -0,0 +1,23 @@ +exports.config = { + specs: [ + './test/spec/e2e/**/*.spec.js', + ], + exclude: [ + // TODO: decipher original intent for "longform" tests + // they all appear to be almost exact copies + './test/spec/e2e/longform/**/*' + ], + logLevel: 'info', // put option here: info | trace | debug | warn| error | silent + bail: 0, + waitforTimeout: 60000, // Default timeout for all waitFor* commands. + connectionRetryTimeout: 60000, // Default timeout in milliseconds for request if Selenium Grid doesn't send response + connectionRetryCount: 3, // Default request retries count + framework: 'mocha', + mochaOpts: { + ui: 'bdd', + timeout: 60000, + compilers: ['js:babel-register'], + }, + // if you see error, update this to spec reporter and logLevel above to get detailed report. + reporters: ['spec'] +} diff --git a/webpack.conf.js b/webpack.conf.js index 0ead550e446..c934b09d439 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -128,10 +128,37 @@ module.exports = { return [lib, def]; }) ); + const core = path.resolve('./src'); + const paapiMod = path.resolve('./modules/paapi.js'); + const nodeModules = path.resolve('./node_modules'); + return Object.assign(libraries, { + common_deps: { + name: 'common_deps', + test(module) { + return module.resource && module.resource.startsWith(nodeModules); + } + }, + core: { + name: 'chunk-core', + test: (module) => { + return module.resource && module.resource.startsWith(core); + } + }, + paapi: { + // fledgeForGpt imports paapi to keep backwards compat for NPM consumers + // this makes the paapi module its own chunk, pulled in by both paapi and fledgeForGpt entry points, + // to avoid duplication + // TODO: remove this in prebid 9 + name: 'chunk-paapi', + test: (module) => { + return module.resource === paapiMod; + } + } + }, { default: false, defaultVendors: false - }) + }); })() } }, diff --git a/webpack.creative.js b/webpack.creative.js new file mode 100644 index 00000000000..86f5f24d580 --- /dev/null +++ b/webpack.creative.js @@ -0,0 +1,25 @@ +const path = require('path'); + +module.exports = { + mode: 'production', + resolve: { + modules: [ + path.resolve('.'), + 'node_modules' + ], + }, + entry: { + 'creative': { + import: './creative/crossDomain.js', + }, + 'renderers/display': { + import: './creative/renderers/display/renderer.js' + }, + 'renderers/native': { + import: './creative/renderers/native/renderer.js' + } + }, + output: { + path: path.resolve('./build/creative'), + }, +}