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 511e78048e4..f17c7a0063d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,6 +42,7 @@ module.exports = { sourceType: 'module', ecmaVersion: 2018, }, + ignorePatterns: ['libraries/creative-renderer*'], rules: { 'comma-dangle': 'off', 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/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/gulpfile.js b/gulpfile.js index 5e16af8b0c1..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,10 +173,29 @@ function makeWebpackPkg(extraConfig = {}) { } } -function buildCreative() { - return gulp.src(['**/*']) - .pipe(webpackStream(require('./webpack.creative.js'))) - .pipe(gulp.dest('build/creative')) +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) { @@ -261,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'; @@ -293,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]; }) @@ -302,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) } @@ -326,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) { @@ -420,6 +461,8 @@ function watchTaskMaker(options = {}) { var mainWatcher = gulp.watch([ 'src/**/*.js', 'libraries/**/*.js', + '!libraries/creative-renderer-*/**/*.js', + 'creative/**/*.js', 'modules/**/*.js', ].concat(options.alsoWatch)); @@ -430,8 +473,8 @@ function watchTaskMaker(options = {}) { } } -const watch = watchTaskMaker({alsoWatch: ['test/**/*.js'], task: () => gulp.series(clean, gulp.parallel(lint, 'build-bundle-dev', test, buildCreative))}); -const watchFast = watchTaskMaker({livereload: false, task: () => gulp.parallel('build-bundle-dev', buildCreative)}); +const watch = watchTaskMaker({alsoWatch: ['test/**/*.js'], task: () => gulp.series(clean, gulp.parallel(lint, 'build-bundle-dev', test))}); +const watchFast = watchTaskMaker({livereload: false, task: () => gulp.series('build-bundle-dev')}); // support tasks gulp.task(lint); @@ -441,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({ @@ -462,23 +508,21 @@ gulp.task('build-bundle-verbose', gulp.series(makeWebpackPkg({ } }), gulpBundle.bind(null, false))); -gulp.task('build-creative', gulp.series(buildCreative, updateCreativeExample)); - // 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.parallel('build-creative', 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', 'build-creative')); +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))); -gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', buildCreative, watchFast))); +gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast))); gulp.task('serve-prod', gulp.series(clean, gulp.parallel('build-bundle-prod', startLocalServer))); gulp.task('serve-and-test', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast, testTaskMaker({watch: true})))); gulp.task('serve-e2e', gulp.series(clean, 'build-bundle-prod', gulp.parallel(() => startIntegServer(), startLocalServer))); @@ -486,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/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/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/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/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index f1c0c647e72..bf2bd5f3fad 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -2,10 +2,10 @@ // creative will be rendered, e.g. GAM delivering a SafeFrame // this code is autogenerated, also available in 'build/creative/creative.js' - + + + + + + + + + +

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/libraries/appnexusUtils/anKeywords.js b/libraries/appnexusUtils/anKeywords.js index d6714dacc21..a6fa8d7a21e 100644 --- a/libraries/appnexusUtils/anKeywords.js +++ b/libraries/appnexusUtils/anKeywords.js @@ -78,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/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/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/creativeRender/constants.js b/libraries/creativeRender/constants.js deleted file mode 100644 index 7b67f8ed5cd..00000000000 --- a/libraries/creativeRender/constants.js +++ /dev/null @@ -1,10 +0,0 @@ -import events from '../../src/constants.json'; - -export const PREBID_NATIVE = 'Prebid Native'; -export const PREBID_REQUEST = 'Prebid Request'; -export const PREBID_RESPONSE = 'Prebid Response'; -export const PREBID_EVENT = 'Prebid Event'; -export const AD_RENDER_SUCCEEDED = events.EVENTS.AD_RENDER_SUCCEEDED; -export const AD_RENDER_FAILED = events.EVENTS.AD_RENDER_FAILED; -export const NO_AD = events.AD_RENDER_FAILED_REASON.NO_AD; -export const EXCEPTION = events.AD_RENDER_FAILED_REASON.EXCEPTION; diff --git a/libraries/creativeRender/crossDomain.js b/libraries/creativeRender/crossDomain.js deleted file mode 100644 index ffa8b468f12..00000000000 --- a/libraries/creativeRender/crossDomain.js +++ /dev/null @@ -1,57 +0,0 @@ -import {mkFrame, writeAd} from './writer.js'; -import { - AD_RENDER_FAILED, - AD_RENDER_SUCCEEDED, - PREBID_EVENT, - PREBID_RESPONSE, - PREBID_REQUEST, - EXCEPTION -} from './constants.js'; - -export function renderer(win = window) { - return function ({adId, pubUrl, clickUrl}) { - const pubDomain = (function() { - const a = win.document.createElement('a'); - a.href = pubUrl; - return a.protocol + '//' + a.host; - })(); - function sendMessage(type, payload, transfer) { - win.parent.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, transfer); - } - function cb(err) { - sendMessage(PREBID_EVENT, { - event: err == null ? AD_RENDER_SUCCEEDED : AD_RENDER_FAILED, - info: err - }); - } - function onMessage(ev) { - let data = {}; - try { - data = JSON.parse(ev[ev.message ? 'message' : 'data']); - } catch (e) { - return; - } - if (data.message === PREBID_RESPONSE && data.adId === adId) { - try { - let doc = win.document - if (data.ad) { - doc = mkFrame(doc, {width: data.width, height: data.height}).contentDocument; - doc.open(); - } - writeAd(data, cb, doc); - } catch (e) { - // eslint-disable-next-line standard/no-callback-literal - cb({ reason: EXCEPTION, message: e.message }) - } - } - } - - const channel = new MessageChannel(); - channel.port1.onmessage = onMessage; - sendMessage(PREBID_REQUEST, { - options: {clickUrl} - }, [channel.port2]); - win.addEventListener('message', onMessage, false); - } -} -window.renderAd = renderer(); diff --git a/libraries/creativeRender/direct.js b/libraries/creativeRender/direct.js deleted file mode 100644 index 19d34e16844..00000000000 --- a/libraries/creativeRender/direct.js +++ /dev/null @@ -1,62 +0,0 @@ -import {emitAdRenderFail, emitAdRenderSucceeded, handleRender} from '../../src/adRendering.js'; -import {writeAd} from './writer.js'; -import {auctionManager} from '../../src/auctionManager.js'; -import CONSTANTS from '../../src/constants.json'; -import {inIframe, insertElement} from '../../src/utils.js'; -import {getGlobal} from '../../src/prebidGlobal.js'; -import {EXCEPTION} from './constants.js'; - -export function renderAdDirect(doc, adId, options) { - let bid; - function cb(err) { - if (err != null) { - emitAdRenderFail(Object.assign({id: adId, bid}, err)); - } else { - emitAdRenderSucceeded({doc, bid, adId}) - } - } - function renderFn(adData) { - writeAd(adData, cb, doc); - if (doc.defaultView && doc.defaultView.frameElement) { - doc.defaultView.frameElement.width = adData.width; - doc.defaultView.frameElement.height = adData.height; - } - // TODO: this is almost certainly the wrong way to do this - const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`); - insertElement(creativeComment, doc, 'html'); - } - try { - if (!adId || !doc) { - // eslint-disable-next-line standard/no-callback-literal - cb({ - reason: CONSTANTS.AD_RENDER_FAILED_REASON.MISSING_DOC_OR_ADID, - message: `missing ${adId ? 'doc' : 'adId'}` - }); - } else { - bid = auctionManager.findBidByAdId(adId); - - if (FEATURES.VIDEO) { - // TODO: could the video module implement this as a custom renderer, rather than a special case in here? - const adUnit = bid && auctionManager.index.getAdUnit(bid); - const videoModule = getGlobal().videoModule; - if (adUnit?.video && videoModule) { - videoModule.renderBid(adUnit.video.divId, bid); - return; - } - } - - if ((doc === document && !inIframe())) { - // eslint-disable-next-line standard/no-callback-literal - cb({ - reason: CONSTANTS.AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, - message: `renderAd was prevented from writing to the main document.` - }) - } else { - handleRender(renderFn, {adId, options: {clickUrl: options?.clickThrough}, bidResponse: bid, doc}); - } - } - } catch (e) { - // eslint-disable-next-line standard/no-callback-literal - cb({reason: EXCEPTION, message: e.message}) - } -} diff --git a/libraries/creativeRender/writer.js b/libraries/creativeRender/writer.js deleted file mode 100644 index 80bb0592a1f..00000000000 --- a/libraries/creativeRender/writer.js +++ /dev/null @@ -1,34 +0,0 @@ -import {NO_AD} from './constants.js'; - -const IFRAME_ATTRS = { - frameBorder: 0, - scrolling: 'no', - marginHeight: 0, - marginWidth: 0, - topMargin: 0, - leftMargin: 0, - allowTransparency: 'true', -}; - -export function mkFrame(doc, attrs) { - const frame = doc.createElement('iframe'); - attrs = Object.assign({}, attrs, IFRAME_ATTRS); - Object.entries(attrs).forEach(([k, v]) => frame.setAttribute(k, v)); - doc.body.appendChild(frame); - return frame; -} - -export function writeAd({ad, adUrl, width, height}, cb, doc = document) { - if (!ad && !adUrl) { - // eslint-disable-next-line standard/no-callback-literal - cb({reason: NO_AD, message: 'Missing ad markup or URL'}); - } else { - if (adUrl && !ad) { - mkFrame(doc, {width, height, src: adUrl}) - } else { - doc.write(ad); - doc.close(); - } - cb(); - } -} 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/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/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/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/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 d2a13a57330..cfa98b5ab32 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -63,6 +63,7 @@ "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", + "azerionedgeRtdProvider", "blueconicRtdProvider", "brandmetricsRtdProvider", "browsiRtdProvider", @@ -102,6 +103,9 @@ "videoModule": [ "jwplayerVideoProvider", "videojsVideoProvider" + ], + "paapi": [ + "fledgeForGpt" ] } } diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 0cb1b1f3382..33086562111 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -5,10 +5,12 @@ * @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 @@ -20,35 +22,42 @@ const MODULE_NAME = '33acrossId'; const API_URL = 'https://lexicon.33across.com/v1/envelope'; const AJAX_TIMEOUT = 10000; const CALLER_NAME = 'pbjs'; +const GVLID = 58; -function getEnvelope(response) { +const STORAGE_FPID_KEY = '33acrossIdFp'; + +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) @@ -69,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 = { /** @@ -80,7 +129,7 @@ export const thirthyThreeAcrossIdSubmodule = { */ name: MODULE_NAME, - gvlid: 58, + gvlid: GVLID, /** * decode the stored id value for passing to bid requests @@ -102,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/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/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 112722383a9..6e3c38e4e85 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -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) }; @@ -795,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) { @@ -994,6 +990,9 @@ export const spec = { 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 = validBidRequests.map(rawBidRequest => { @@ -1183,7 +1182,8 @@ export const spec = { coppa: coppa, ccpa: uspConsent, gpp: gppConsent.gpp, - gppSid: gppConsent.gppSid + gppSid: gppConsent.gppSid, + dsa: dsa // populated if exists }, schain: schain, user: { @@ -1220,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/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 e5b40f66176..881b1adfcc4 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -64,6 +64,7 @@ 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; @@ -179,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', @@ -201,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, @@ -214,7 +220,8 @@ export const spec = { dealId: bidResponse.dealid, meta: { mediaType, - advertiserDomains: bidResponse.adomain + advertiserDomains: bidResponse.adomain, + dsa } }; @@ -223,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/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index b40378c8e35..e0538fe2815 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -6,6 +6,14 @@ 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'; export const spec = { diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index add24772463..491808f2602 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -23,12 +23,16 @@ 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', 'plcmt', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', 'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed']; const VIDEO_FPD = ['battr', 'pos']; @@ -107,7 +111,9 @@ export const spec = { {code: 'didnadisplay'}, {code: 'qortex'}, {code: 'adpluto'}, - {code: 'headbidder'} + {code: 'headbidder'}, + {code: 'digiad'}, + {code: 'monetix'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 5afe7781d7d..6c268b2d382 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -2,6 +2,14 @@ import {getValue, formatQS, logError, deepAccess, isArray, getBidIdParameter} fr import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.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: { @@ -26,11 +34,13 @@ export const OPENRTB = { 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, NATIVE], /** @@ -183,38 +193,45 @@ 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: { - 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; - } else if (resbid.mediaType === 'native') { - resbid.native = interpretNativeAd(bid.party_tag) - }; + 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; + } 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; @@ -265,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 = {}; 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/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 a498d056513..eb5f3c19dea 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -103,12 +103,18 @@ const storageTool = (function () { storage.setDataInLocalStorage(META_DATA_KEY, JSON.stringify(metaDataForSaving)); }; - const getUsi = function (meta, ortb2) { - let usi = (meta && meta.usi) ? meta.usi : false; + 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) { - usi = ortb2.user.id + return ortb2.user.id } - return usi; + return (meta && meta.usi) ? meta.usi : false } const getSegmentsFromOrtb = function (ortb2) { @@ -131,7 +137,7 @@ const storageTool = (function () { refreshStorage: function (bidderRequest) { const ortb2 = bidderRequest.ortb2 || {}; metaInternal = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); - metaInternal.usi = getUsi(metaInternal, ortb2); + metaInternal.usi = getUsi(metaInternal, ortb2, bidderRequest); if (!metaInternal.usi) { delete metaInternal.usi; } diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index b48a7ec43b0..9f2810e13df 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -7,6 +7,23 @@ 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; diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index 6b0d642468b..f19cf020ca8 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -1,12 +1,22 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {buildUrl, logInfo, parseSizesInput, triggerPixel} from '../src/utils.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; @@ -18,7 +28,6 @@ export const spec = { supportedMediaTypes: [BANNER], /** - * f * @param {object} bid * @return {boolean} */ @@ -33,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, @@ -54,8 +71,8 @@ export const spec = { * @return {Bid[]} */ interpretResponse: (response, request) => { - logInfo(request); - logInfo(response); + logMessage(request); + logMessage(response); const res = response && response.body && response.body.data; let bidResponses = []; @@ -117,11 +134,9 @@ export const spec = { */ onBidWon: (bid) => { logInfo('onBidWon', bid); - - const bidString = JSON.stringify(bid); - let copyOfBid = JSON.parse(bidString); - delete copyOfBid.ad; - const shortBidString = JSON.stringify(bid); + let copyOfBid = { ...bid } + delete copyOfBid.ad + const shortBidString = JSON.stringify(copyOfBid); const encodedBuf = window.btoa(shortBidString); let params = { @@ -161,21 +176,48 @@ 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) }]; } }; @@ -197,7 +239,7 @@ function buildRequest(validBidRequests, bidderRequest) { // onetime User ID const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); userId = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); - logInfo('generated onetime User ID: ', userId); + logMessage('generated onetime User ID: ', userId); window.qid = userId; } diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index eb00011593d..43795b3caba 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -8,7 +8,7 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import {isFn, isPlainObject, isStr, logError, logInfo} from '../src/utils.js'; +import {isFn, isPlainObject, isStr, logError, logInfo, logMessage} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; /** @@ -66,7 +66,18 @@ export const adqueryIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { - logInfo('adqueryIdSubmodule getId'); + logMessage('adqueryIdSubmodule getId'); + + let qid = storage.getDataFromLocalStorage('qid'); + + if (qid) { + return { + callback: function (callback) { + callback(qid); + } + } + } + if (!isPlainObject(config.params)) { config.params = {}; } diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index 3c9c661b09c..68cd859e24e 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -23,6 +23,11 @@ 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'; const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration', diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index 1da75f2063d..2dab76b7862 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -42,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/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 index 4b22d568785..a0c67ddac7e 100644 --- a/modules/adstirBidAdapter.js +++ b/modules/adstirBidAdapter.js @@ -36,6 +36,7 @@ export const spec = { 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', []), diff --git a/modules/adstirBidAdapter.md b/modules/adstirBidAdapter.md index 7485375a09d..5840697a9b0 100644 --- a/modules/adstirBidAdapter.md +++ b/modules/adstirBidAdapter.md @@ -10,6 +10,8 @@ Maintainer: support@ad-stir.com Module that connects to adstir's demand sources +Prebid.js version 8.24.0 or above is required to use this adapter. + # Test Parameters ``` diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 8e6aeecdd75..a95b9ed5652 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -7,6 +7,11 @@ 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/'; const PROTOCOL = 'https://'; @@ -115,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/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 49187da2fe2..fdc1249ded4 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -4,6 +4,11 @@ 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; export const ENDPOINT_URL_PUBLISHER_PLACEHOLDER = '{PUBLISHER}'; diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 8952c3ae2b9..ad1c0af039e 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -5,6 +5,12 @@ 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'; @@ -57,7 +63,8 @@ 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) { diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 2184544807e..079628c88fc 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -105,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 9e2d4efb5ff..e02ab920707 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -3,6 +3,11 @@ import { registerBidder } from '../src/adapters/bidderFactory.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'; const SDKType = 5; @@ -66,10 +71,8 @@ export const spec = { queryString = tryAppendQueryString(queryString, 'cdep', bidRequest.ortb2?.device?.ext?.cdep) queryString = tryAppendQueryString(queryString, 'prebid_id', bidRequest.bidId); queryString = tryAppendQueryString(queryString, 'prebid_ver', '$prebid.version$'); - - if (pageUrl) { - queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); - } + queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); + queryString = tryAppendQueryString(queryString, 'schain', spec.serializeSupplyChain(bidRequest.schain || [])) const adFormatIDs = pickAdFormats(bidRequest) if (adFormatIDs && adFormatIDs.length > 0) { @@ -167,6 +170,31 @@ export const spec = { return syncs; }, + + /** + * 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)}` + }, + + /** + * 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('!') + } } function pickAdFormats(bidRequest) { 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 53f6460434f..d4e7cab8ed1 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -43,7 +43,11 @@ export const spec = { 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 = { requestId: generateUUID(), @@ -59,6 +63,8 @@ export const spec = { h: screen.height }, ortb2: { + source, + user, site: { keywords: bidderRequest.ortb2?.site?.keywords }, diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index 834df134c2e..dadbdb72e95 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -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 c6230d9f1e4..590ad264515 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -38,12 +38,17 @@ import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils 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']; @@ -67,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 = { @@ -346,6 +356,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; } @@ -404,11 +438,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; } @@ -585,6 +615,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 = { @@ -887,15 +921,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; } @@ -954,8 +988,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'; } } @@ -973,6 +1011,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; } diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index 704cffefb39..a4a6c78566e 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -1,32 +1,20 @@ -import { - _each, - deepAccess, - deepSetValue, - getDNT, - inIframe, - isArray, - isFn, - logWarn, - parseSizesInput -} 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/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 e716fe94c8b..92a4343b3ed 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -12,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 index 7d7bd8cb34c..436418e7597 100644 --- a/modules/automatadAnalyticsAdapter.js +++ b/modules/automatadAnalyticsAdapter.js @@ -14,7 +14,7 @@ import { config } from '../src/config.js' const ADAPTER_CODE = 'automatadAnalytics' const trialCountMilsMapping = [1500, 3000, 5000, 10000]; -var isLoggingEnabled; var queuePointer = 0; var retryCount = 0; var timer = null; var __atmtdAnalyticsQueue = []; +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) { @@ -134,6 +134,9 @@ const processEvents = () => { 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 } } @@ -142,22 +145,18 @@ const addGPTHandlers = () => { googletag.cmd = googletag.cmd || [] googletag.cmd.push(() => { googletag.pubads().addEventListener('slotRenderEnded', (event) => { - if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { - if (window.__atmtdAggregatorFirstAuctionInitialized === true) { - window.atmtdAnalytics.slotRenderEndedGPTHandler(event) - return; - } + 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) { - if (window.__atmtdAggregatorFirstAuctionInitialized === true) { - window.atmtdAnalytics.impressionViewableHandler(event) - return; - } + 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`) @@ -167,6 +166,7 @@ const addGPTHandlers = () => { const initializeQueue = () => { self.__atmtdAnalyticsQueue.push = (args) => { + self.qBeingUsed = true Array.prototype.push.apply(self.__atmtdAnalyticsQueue, [args]); if (timer) { clearTimeout(timer); @@ -196,9 +196,10 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { }, track({eventType, args}) { + const shouldNotPushToQueue = !self.qBeingUsed switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: - if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler && shouldNotPushToQueue) { self.prettyLog('status', 'Aggregator loaded, initialising auction through handlers'); window.atmtdAnalytics.auctionInitHandler(args); } else { @@ -207,7 +208,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.BID_REQUESTED: - if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler && shouldNotPushToQueue) { window.atmtdAnalytics.bidRequestedHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -215,7 +216,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.BID_REJECTED: - if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler && shouldNotPushToQueue) { window.atmtdAnalytics.bidRejectedHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -223,7 +224,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.BID_RESPONSE: - if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler && shouldNotPushToQueue) { window.atmtdAnalytics.bidResponseHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -231,7 +232,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.BIDDER_DONE: - if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler && shouldNotPushToQueue) { window.atmtdAnalytics.bidderDoneHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -239,7 +240,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.BID_WON: - if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler && shouldNotPushToQueue) { window.atmtdAnalytics.bidWonHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -247,7 +248,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.NO_BID: - if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler && shouldNotPushToQueue) { window.atmtdAnalytics.noBidHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -255,7 +256,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.AUCTION_DEBUG: - if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler && shouldNotPushToQueue) { window.atmtdAnalytics.auctionDebugHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -263,7 +264,7 @@ let atmtdAdapter = Object.assign({}, baseAdapter, { } break; case CONSTANTS.EVENTS.BID_TIMEOUT: - if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler && shouldNotPushToQueue) { window.atmtdAnalytics.bidderTimeoutHandler(args); } else { self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); @@ -304,7 +305,7 @@ atmtdAdapter.enableAnalytics = function (configuration) { atmtdAdapter.originEnableAnalytics(configuration) }; -/// /////////// ADAPTER REGISTRATION ////////////// +/// /////////// ADAPTER REGISTRATION ///////////// adapterManager.registerAnalyticsAdapter({ adapter: atmtdAdapter, @@ -319,7 +320,15 @@ export var self = { prettyLog, queuePointer, retryCount, - isLoggingEnabled + isLoggingEnabled, + qBeingUsed, + qTraversalComplete +} + +window.__atmtdAnalyticsGlobalObject = { + q: self.__atmtdAnalyticsQueue, + qBeingUsed: self.qBeingUsed, + qTraversalComplete: self.qTraversalComplete } export default atmtdAdapter; 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/beopBidAdapter.js b/modules/beopBidAdapter.js index ad52af824ee..0b2a965448b 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -12,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; @@ -25,7 +31,7 @@ export const spec = { /** * Test if the bid request is valid. * - * @param {bid} : The Bid params + * @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) { @@ -41,8 +47,8 @@ export const spec = { /** * Create a BeOp server request from a list of BidRequest * - * @param {validBidRequests[], ...} : The array of validated bidRequests - * @param {... , bidderRequest} : Common params for each bidRequests + * @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) { @@ -72,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); @@ -159,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 6883b7cce2c..d2010f22e1a 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -3,6 +3,15 @@ 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'; const CODE_TYPES = ['inpage', 'preroll', 'midroll', 'postroll']; @@ -23,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/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 a29976cfcb7..7ae1ccf9217 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -1,7 +1,14 @@ import {_each, isArray, deepClone, getUniqueIdentifierStr, getBidIdParameter} 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 + * @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/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index debe956d7eb..37c99878d68 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -153,7 +153,7 @@ export function getDomLoadingDuration() { } /** - * @param bidRequest + * @param bidResponse * @return {({cpm, netRevenue: boolean, requestId, width: number, currency, ttl: number, creativeId, height: number}&{mediaType: string, vastXml})|null} */ export const buildBid = (bidResponse) => { @@ -285,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) => { diff --git a/modules/blueconicRtdProvider.js b/modules/blueconicRtdProvider.js index 6b10e79b94c..c09fc6ee34c 100644 --- a/modules/blueconicRtdProvider.js +++ b/modules/blueconicRtdProvider.js @@ -37,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)) { @@ -82,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/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 17336baa76c..2d9dcdfdf48 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -78,6 +78,7 @@ function checkConsent (userConsent) { /** * Add event- listeners to hook in to brandmetrics events * @param {Object} reqBidsConfigObj + * @param {Object} moduleConfig * @param {function} callback */ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { @@ -114,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 */ 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/britepoolIdSystem.js b/modules/britepoolIdSystem.js index 37ace544dc7..dcc365faaac 100644 --- a/modules/britepoolIdSystem.js +++ b/modules/britepoolIdSystem.js @@ -38,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 5281274616a..ab3db2a5d20 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -191,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) { diff --git a/modules/bucksenseBidAdapter.js b/modules/bucksenseBidAdapter.js index ecf27de997d..5aa14f2a53b 100644 --- a/modules/bucksenseBidAdapter.js +++ b/modules/bucksenseBidAdapter.js @@ -2,6 +2,11 @@ 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/'; 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/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 4fef3fb2494..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'; @@ -24,7 +33,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: function(validBidRequests, bidderRequest) { 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 c7d8fa5797c..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'; 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/concertBidAdapter.js b/modules/concertBidAdapter.js index bc2c4555c54..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'; diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js index 7524cd4e194..0b840db6c26 100644 --- a/modules/connatixBidAdapter.js +++ b/modules/connatixBidAdapter.js @@ -98,6 +98,7 @@ export const spec = { ortb2: bidderRequest.ortb2, gdprConsent: bidderRequest.gdprConsent, uspConsent: bidderRequest.uspConsent, + gppConsent: bidderRequest.gppConsent, refererInfo: bidderRequest.refererInfo, bidRequests, }; 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 f696ce25902..416430fb1c9 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -455,7 +455,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) { config = config && config.gpp; diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 1218c3724f4..78ec13cb891 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -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 696549a67dc..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'; 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/conversantBidAdapter.js b/modules/conversantBidAdapter.js index c338a6633b5..ebcad38d866 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -21,6 +21,13 @@ 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'; @@ -87,7 +94,10 @@ const converter = ortbConverter({ 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) { diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index a2a054d7659..74e732d313f 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,7 +1,6 @@ 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'; @@ -186,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 2fbf3d9ab9e..c1fcac4ae2f 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -11,14 +11,19 @@ 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; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; -const FLEDGE_SELLER_DOMAIN = 'https://grid-mercury.criteo.com'; -const FLEDGE_SELLER_TIMEOUT = 500; -const FLEDGE_DECISION_LOGIC_URL = 'https://grid-mercury.criteo.com/fledge/decision'; export const PROFILE_ID_PUBLISHERTAG = 185; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; @@ -250,6 +255,9 @@ export const spec = { if (slot.ext?.meta?.networkName) { bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) } + if (slot.ext?.dsa?.adrender) { + bid.meta = Object.assign({}, bid.meta, { adrender: slot.ext.dsa.adrender }) + } if (slot.native) { if (bidRequest.params.nativeCallback) { bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); @@ -273,41 +281,13 @@ export const spec = { }); } - if (isArray(body.ext?.igbid)) { - const seller = body.ext.seller || FLEDGE_SELLER_DOMAIN; - const sellerTimeout = body.ext.sellerTimeout || FLEDGE_SELLER_TIMEOUT; - body.ext.igbid.forEach((igbid) => { - const perBuyerSignals = {}; - igbid.igbuyer.forEach(buyerItem => { - perBuyerSignals[buyerItem.origin] = buyerItem.buyerdata; - }); - const bidRequest = request.bidRequests.find(b => b.bidId === igbid.impid); - const bidId = bidRequest.bidId; - let sellerSignals = body.ext.sellerSignals || {}; - if (!sellerSignals.floor && bidRequest.params.bidFloor) { - sellerSignals.floor = bidRequest.params.bidFloor; + if (isArray(body.ext?.igi)) { + body.ext.igi.forEach((igi) => { + if (isArray(igi?.igs)) { + igi.igs.forEach((igs) => { + fledgeAuctionConfigs.push(igs); + }); } - if (!sellerSignals.sellerCurrency && bidRequest.params.bidFloorCur) { - sellerSignals.sellerCurrency = bidRequest.params.bidFloorCur; - } - if (body?.ext?.sellerSignalsPerImp !== undefined) { - const sellerSignalsPerImp = body.ext.sellerSignalsPerImp[bidId]; - if (sellerSignalsPerImp !== undefined) { - sellerSignals = {...sellerSignals, ...sellerSignalsPerImp}; - } - } - fledgeAuctionConfigs.push({ - bidId, - config: { - seller, - sellerSignals, - sellerTimeout, - perBuyerSignals, - auctionSignals: {}, - decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, - interestGroupBuyers: Object.keys(perBuyerSignals), - }, - }); }); } @@ -495,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; diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index f158e16a64e..f878be5f66a 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -4,12 +4,19 @@ 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'; 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. @@ -134,6 +141,7 @@ function getCwExtension() { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER], /** diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index 127e7893ec5..db795c89155 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -15,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/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 7c24cd6a8f6..0a64ed88ca5 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -98,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/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index a3e26dc7202..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', diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 551c78c1e24..de2fd3c3a94 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -3,6 +3,12 @@ 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; @@ -13,10 +19,11 @@ 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 NATIVERET = { id: 'id', @@ -58,7 +65,7 @@ const NATIVERET = { }; /** - * get page title + * get page title111 * @returns {string} */ @@ -126,12 +133,11 @@ export const getPmgUID = () => { let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID); if (!pmgUid) { pmgUid = utils.generateUUID(); - const date = new Date(); - date.setTime(date.getTime() + COOKIE_RETENTION_TIME); - try { - storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, date.toUTCString()); - } catch (e) {} } + // Extend the expiration time of pmguid + try { + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString()); + } catch (e) {} return pmgUid; }; @@ -295,6 +301,16 @@ function getReferrer(bidRequest = {}, bidderRequest = {}) { 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 * @@ -421,6 +437,7 @@ 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(); @@ -447,6 +464,7 @@ function getParam(validBidRequests, bidderRequest) { 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, @@ -511,7 +529,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; }, /** @@ -522,6 +540,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); @@ -640,6 +660,20 @@ export const spec = { } 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', 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 index 17865e6793a..57aafd47fc8 100644 --- a/modules/dsp_genieeBidAdapter.js +++ b/modules/dsp_genieeBidAdapter.js @@ -3,6 +3,16 @@ 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'; @@ -44,7 +54,7 @@ export const spec = { /** * Determines whether or not the given bid request is valid. * - * @param {BidRequest} - The bid params to validate. + * @param {BidRequest} _ The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (_) { @@ -53,8 +63,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids - * @param {bidderRequest} - the master bidRequest object + * @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) { 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 index 282b54e0823..d803c476b6d 100644 --- a/modules/dxkultureBidAdapter.js +++ b/modules/dxkultureBidAdapter.js @@ -12,6 +12,11 @@ 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; @@ -91,7 +96,7 @@ export const spec = { /** * Determines whether or not the given bid request is valid. * - * @param {BidRequest} bidRequest The bid params to validate. + * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { @@ -264,7 +269,7 @@ function _validateParams(bidRequest) { /** * Validates banner bid request. If it is not banner media type returns true. - * @param {object} bid, bid to validate + * @param {BidRequest} bidRequest bid to validate * @return boolean, true if valid, otherwise false */ function _validateBanner(bidRequest) { @@ -282,7 +287,7 @@ function _validateBanner(bidRequest) { /** * Validates video bid request. If it is not video media type returns true. - * @param {object} bid, bid to validate + * @param {BidRequest} bidRequest, bid to validate * @return boolean, true if valid, otherwise false */ function _validateVideo(bidRequest) { diff --git a/modules/dynamicAdBoostRtdProvider.js b/modules/dynamicAdBoostRtdProvider.js index fe08795f313..697bd7340d3 100644 --- a/modules/dynamicAdBoostRtdProvider.js +++ b/modules/dynamicAdBoostRtdProvider.js @@ -9,6 +9,10 @@ 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 && 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/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 81b8c5d8058..ce01abb9e71 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -4,6 +4,10 @@ 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' const ENDPOINT = 'https://sspback.eskimi.com/bid-request' diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index fa6113250a8..d98dc02cdce 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -14,13 +14,6 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // eslint-disable-next-line prebid/validate-imports import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.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').euidId} euidId - */ - const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; const PREBID_VERSION = '$prebid.version$'; @@ -110,6 +103,7 @@ export const euidIdSubmodule = { mappedConfig.cstg = { serverPublicKey: config?.params?.serverPublicKey, subscriptionId: config?.params?.subscriptionId, + optoutCheck: 1, ...extractIdentityFromParams(config?.params ?? {}) } } @@ -135,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 72e40b8ce7b..9c3f730da83 100644 --- a/modules/euidIdSystem.md +++ b/modules/euidIdSystem.md @@ -4,10 +4,10 @@ The EUID module handles storing, providing, and optionally refreshing tokens. Wh *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 is 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: +*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=uid2IdSystem --disable UID2_CSTG + $ 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) @@ -157,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/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 7162f4e9230..bda4494faaf 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -1,169 +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 {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 {submodule} from '../src/hook.js'; +import {deepAccess, logInfo, logWarn} from '../src/utils.js'; import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; - -const MODULE = 'fledgeForGpt' -const PENDING = {}; - -export let isEnabled = false; - -config.getConfig('fledgeForGpt', config => init(config.fledgeForGpt)); - -/** - * Module init. - */ -export function init(cfg) { - if (cfg && cfg.enabled === true) { - if (!isEnabled) { - getHook('addComponentAuction').before(addComponentAuctionHook); - getHook('makeBidRequests').after(markForFledge); - events.on(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); - events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); - isEnabled = true; - } - logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg); - } else { - if (isEnabled) { - getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove(); - getHook('makeBidRequests').getHooks({hook: markForFledge}).remove() - events.off(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); - events.off(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); - isEnabled = false; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + +// 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'; + +let getPAAPIConfig; + +// 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; + +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 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); } - logInfo(`${MODULE} disabled`, cfg); - } -} - -function setComponentAuction(adUnitCode, auctionConfigs) { - const gptSlot = getGptSlotForAdUnitCode(adUnitCode); - if (gptSlot && gptSlot.setConfig) { - gptSlot.setConfig({ - componentAuction: auctionConfigs.map(cfg => ({ - configKey: cfg.seller, - auctionConfig: cfg - })) - }); - logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); - } else { - logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); - } + }; } -function onAuctionInit({auctionId}) { - PENDING[auctionId] = {}; -} +const setComponentAuction = slotConfigurator(); -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); +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); + }); + } } - return cfg; } -function onAuctionEnd({auctionId, bidsReceived, bidderRequests}) { - try { - const allReqs = bidderRequests?.flatMap(br => br.bids); - Object.entries(PENDING[auctionId]).forEach(([adUnitCode, auctionConfigs]) => { - const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; - const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); - setComponentAuction(adUnitCode, auctionConfigs.map(cfg => mergeDeep({}, slotSignals, 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); }) - } finally { - delete PENDING[auctionId]; - } -} - -function setFPDSignals(auctionConfig, fpd) { - auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); -} - -export function addComponentAuctionHook(next, request, componentAuctionConfig) { - const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; - if (PENDING.hasOwnProperty(auctionId)) { - setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); - !PENDING[auctionId].hasOwnProperty(adUnitCode) && (PENDING[auctionId][adUnitCode] = []); - PENDING[auctionId][adUnitCode].push(componentAuctionConfig); - } else { - logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig) - } - next(request, componentAuctionConfig); -} - -function isFledgeSupported() { - return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator -} - -export function markForFledge(next, bidderRequests) { - if (isFledgeSupported()) { - const globalFledgeConfig = config.getConfig('fledgeForGpt'); - const bidders = globalFledgeConfig?.bidders ?? []; - bidderRequests.forEach((bidderReq) => { - const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length === 0 || bidders.includes(bidderReq.bidderCode)); - config.runWithBidder(bidderReq.bidderCode, () => { - const fledgeEnabled = config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined); - const defaultForSlots = config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined); - Object.assign(bidderReq, {fledgeEnabled}); - bidderReq.bids.forEach(bidReq => { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? defaultForSlots) }) - }) - }); - } - 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); + 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/flippBidAdapter.js b/modules/flippBidAdapter.js index ea5e71ad81d..f9c424d9da5 100644 --- a/modules/flippBidAdapter.js +++ b/modules/flippBidAdapter.js @@ -3,6 +3,16 @@ 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]; @@ -97,7 +107,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {BidRequest[]} validBidRequests[] an array of bids + * @param {validBidRequests} validBidRequests an array of bids * @param {BidderRequest} bidderRequest master bidRequest object * @return ServerRequest Info describing the request to the server. */ diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index b566769c00e..c0ae55efc89 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -2,6 +2,12 @@ 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'; @@ -25,8 +31,8 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids. - * @param {bidderRequest} bidderRequest bidder request object. + * @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) => { diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 73c56160b00..e11aa3f8fb7 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -3,6 +3,11 @@ 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; @@ -477,7 +482,7 @@ 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. + * @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) { 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/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index a2ed71a898c..0b0d9027c03 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -56,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) { diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 8cffccb9f30..a8888893333 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,6 +1,11 @@ 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'; @@ -106,8 +111,8 @@ 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 = { diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index 86cbb1d89de..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) { diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index 559f9f77aaf..d7af51f7312 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -12,6 +12,16 @@ 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'; @@ -32,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 8892df130df..9f9913b7023 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -19,7 +19,6 @@ 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'; @@ -30,6 +29,11 @@ import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils 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'; const PRICING_URL = 'https://templates.da-services.ch/01_universal/burda_prebid/1.0/json/sizeCPMMapping.json'; @@ -877,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); } 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/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 5d1f35f24ff..b881e868bf3 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils. const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '2.0.0'; +export const ANALYTICS_VERSION = '2.2.0'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; @@ -27,19 +27,24 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const isSampled = function(greenbidsId, samplingRate) { +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); - - return hashInt < samplingRate * (0xFFFF + 1); + 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); @@ -59,8 +64,6 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER if (typeof analyticsOptions.options.sampling === 'number') { logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.'); analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling; - // Set sampling to null to prevent prebid analytics integrated sampling to happen - analyticsOptions.options.sampling = null; } /** @@ -71,6 +74,14 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER 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; @@ -175,7 +186,7 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); cachedAuction.greenbidsId = generateUUID(); } - cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling); + cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling, this.exploratorySamplingSplit); }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); @@ -228,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/gridBidAdapter.js b/modules/gridBidAdapter.js index 9673633a0fe..d56639ed714 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -16,6 +16,12 @@ 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_c2s' @@ -355,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) => { @@ -460,6 +476,7 @@ export const spec = { }, ajaxCall: function(url, cb, data, options) { + options.browsingTopics = false; return ajax(url, cb, data, options); }, @@ -525,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 }; @@ -537,6 +554,10 @@ function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bid bidResponse.meta.demandSource = serverBid.ext.bidder.grid.demandSource; } + if (serverBid.ext && serverBid.ext.dsa && serverBid.ext.dsa.adrender) { + bidResponse.meta.adrender = serverBid.ext.dsa.adrender; + } + if (serverBid.content_type === 'video') { if (serverBid.adm) { bidResponse.vastXml = serverBid.adm; diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index 539a20a7302..be20ab89130 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -5,12 +5,9 @@ * @requires module:modules/userId */ -import {logError, logInfo, pick} 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'; -import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -19,74 +16,10 @@ import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.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} - */ -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); - } -} - -/** - * 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; - } -} - /** @type {Submodule} */ export const growthCodeIdSubmodule = { /** @@ -103,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; + ids = ids.concat(gcEid) } - if (!sharedId) { - logError('User ID - Publisher ID is not correctly setup.'); - } - - 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/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index deee906298e..7f8627ec5f7 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']; @@ -544,10 +553,11 @@ function interpretResponse(serverResponse, bidRequest) { 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 01b0283d3d1..66cb5624a38 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -25,7 +25,7 @@ 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. 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/id5IdSystem.js b/modules/id5IdSystem.js index b10f3c5d2cf..42f0044edc3 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -10,6 +10,7 @@ import { deepSetValue, isEmpty, isEmptyStr, + isPlainObject, logError, logInfo, logWarn, @@ -21,8 +22,8 @@ import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.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'; +import {GreedyPromise} from '../src/utils/promise.js'; +import {loadExternalScript} from '../src/adloader.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -39,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 @@ -51,26 +53,26 @@ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleNam * @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 @@ -146,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': @@ -232,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) { @@ -240,6 +250,20 @@ 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; + } + } + } }, }; diff --git a/modules/illuminBidAdapter.js b/modules/illuminBidAdapter.js index 36ff7bea0a3..45b74bec5c9 100644 --- a/modules/illuminBidAdapter.js +++ b/modules/illuminBidAdapter.js @@ -7,6 +7,7 @@ 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; @@ -327,6 +328,7 @@ export const spec = { code: BIDDER_CODE, version: BIDDER_VERSION, supportedMediaTypes: [BANNER, VIDEO], + gvlid: GVLID, isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index 4c0b9801a36..ea446bd150d 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -6,6 +6,14 @@ 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']; const DEFAULT_CURRENCY = 'USD'; diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index b563faf52ac..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; diff --git a/modules/incrxBidAdapter.js b/modules/incrxBidAdapter.js index 46be7fb75d6..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'; 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 3ea95ef8443..4d9b95e5948 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -128,6 +128,10 @@ function buildVideo(bidRequest) { } } + if (plcmt) { + optionalParams['plcmt'] = plcmt; + } + let videoObj = { placement, mimes, @@ -137,9 +141,6 @@ function buildVideo(bidRequest) { ...videoBidderParams // bidder specific overrides for video } - if (plcmt) { - videoObj['plcmt'] = plcmt; - } return videoObj } @@ -386,16 +387,16 @@ function validateBanner(bid) { } function validateVideo(bid) { - const videoParams = deepAccess(bid, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bid, 'params.video', {}); + const videoParams = deepAccess(bid, 'mediaTypes.video'); + const videoBidderParams = deepAccess(bid, 'params.video'); let video = { ...videoParams, ...videoBidderParams // bidder specific overrides for video } - // Check if the merged video object is empty - if (Object.keys(video).length === 0 || video === undefined) { - logError('insticator: video object is empty'); - return false; // or handle the case where video parameters are undefined or empty + + // Check if the video object is undefined + if (videoParams === undefined) { + return true; } let w = deepAccess(bid, 'mediaTypes.video.w'); 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 79c6533d122..949483ea7bf 100644 --- a/modules/integr8BidAdapter.js +++ b/modules/integr8BidAdapter.js @@ -3,6 +3,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 = 'integr8'; const DEFAULT_ENDPOINT_URL = 'https://central.sea.integr8.digital/bid'; const DIMENSION_SEPARATOR = 'x'; 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 1d608d7136b..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', 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/ivsBidAdapter.js b/modules/ivsBidAdapter.js index 6f4c024f09f..3deebf9bff3 100644 --- a/modules/ivsBidAdapter.js +++ b/modules/ivsBidAdapter.js @@ -4,6 +4,13 @@ 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 4cc6807b670..a29c1a39bff 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -8,6 +8,9 @@ import { isEmpty, isFn, isInteger, + isNumber, + isStr, + isPlainObject, logError, logWarn, mergeDeep, @@ -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 @@ -111,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: {}, @@ -518,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; } @@ -657,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; + } } } } @@ -710,7 +726,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // getting ixdiags for adunits of the video, outstream & multi format (MF) style const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') let ixdiag = buildIXDiag(validBidRequests, fledgeEnabled); - for (var key in ixdiag) { + for (let key in ixdiag) { r.ext.ixdiag[key] = ixdiag[key]; } @@ -796,6 +812,9 @@ 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' && @@ -1199,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) @@ -1207,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; @@ -1274,6 +1318,7 @@ function buildIXDiag(validBidRequests, fledgeEnabled) { .map(bidRequest => bidRequest.adUnitCode) .filter((value, index, arr) => arr.indexOf(value) === index); + let allEids = deepAccess(validBidRequests, '0.userIdAsEids', []) let ixdiag = { mfu: 0, bu: 0, @@ -1286,7 +1331,8 @@ function buildIXDiag(validBidRequests, fledgeEnabled) { userIds: _getUserIds(validBidRequests[0]), url: window.location.href.split('?')[0], vpd: defaultVideoPlacement, - ae: fledgeEnabled + ae: fledgeEnabled, + eidLength: allEids.length }; // create ad unit map and collect the required diag properties diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index 75268e9d168..1e07ce6b5d8 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -295,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/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 573ff391dae..e8c1c445816 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -192,18 +192,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 { @@ -225,8 +229,8 @@ export function getVatFromCache(mediaID) { }; } -export function getVatFromPlayer(playerID, mediaID) { - const player = getPlayer(playerID); +export function getVatFromPlayer(playerDivId, mediaID) { + const player = getPlayer(playerDivId); if (!player) { return null; } @@ -362,14 +366,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..a4c2f3621e1 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. diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 9d8c7bc06a1..e86d7022987 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'; @@ -95,13 +95,16 @@ 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) }); - if (firstBidRequest.ortb2 != null) { - krakenParams.site = { - cat: firstBidRequest.ortb2.site.cat + // 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 @@ -221,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, @@ -459,10 +462,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; } @@ -482,17 +481,38 @@ function getImpression(bid) { } } - 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) { + const { banner, video, native } = bid.mediaTypes; + + if (banner) { + imp.banner = banner; + } + + if (video) { + imp.video = video; } - if (bid.mediaTypes.video != null) { - imp.video = bid.mediaTypes.video; + if (native) { + imp.native = native; } - if (bid.mediaTypes.native != null) { - imp.native = bid.mediaTypes.native; + 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; } } 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/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index 9a336b16136..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); diff --git a/modules/lemmaDigitalBidAdapter.js b/modules/lemmaDigitalBidAdapter.js index 8a3b05b7ed3..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'; 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..acc76014abe 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'; /** 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 ad71a584e42..786feeb8052 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -11,7 +11,9 @@ import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/val 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 @@ -234,6 +236,12 @@ export const liveIntentIdSubmodule = { 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 } @@ -273,6 +281,7 @@ export const liveIntentIdSubmodule = { return { callback: result }; }, eids: { + ...UID1_EIDS, ...UID2_EIDS, 'lipb': { getValue: function(data) { 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 index 9ba26052727..7c3085047c4 100644 --- a/modules/lm_kiviadsBidAdapter.js +++ b/modules/lm_kiviadsBidAdapter.js @@ -4,6 +4,14 @@ 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'; 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/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 b9665b93494..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), @@ -330,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 } } @@ -671,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'); @@ -687,6 +706,7 @@ magniteAdapter.disableAnalytics = function () { endpoint = undefined; accountId = undefined; resetConfs(); + getHook('callPrebidCache').getHooks({ hook: callPrebidCacheHook }).remove(); magniteAdapter.originDisableAnalytics(); }; @@ -749,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; } } @@ -808,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 1d50ad4e4c1..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'; 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 1fdd3530fae..5e31f60d3b5 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -20,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'; @@ -37,6 +36,11 @@ import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils 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'; const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid'; @@ -865,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); } diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index a9c670f35d8..8f687d30ff3 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -8,43 +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 = 'https://gbid.mediago.io/api/bid?tn='; -const COOKY_SYNC_URL = 'https://gtrace.mediago.io/ju/cs/eplist'; +// 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_'; -const STORE_MAX_AGE = 1000 * 60 * 60 * 24 * 365; +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} */ -const getUserID = () => { - const i = storage.getCookie(COOKIE_KEY_MGUID); +export function getPageDescription(win = window) { + let element; - if (i === null) { - const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid, STORE_MAX_AGE); - return uuid; + 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 i; + + 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} + */ +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} @@ -283,6 +355,16 @@ function getReferrer(bidRequest = {}, bidderRequest = {}) { 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请求参数 * @@ -317,6 +399,10 @@ function getParam(validBidRequests, bidderRequest) { 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 = { @@ -344,11 +430,23 @@ function getParam(validBidRequests, bidderRequest) { firstPartyData, content, cat, - reqTimes + 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(), - id: sharedid || pubcid + buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, + id: sharedid || pubcid, }, eids, site: { @@ -407,13 +505,12 @@ export const spec = { return { method: 'POST', url: ENDPOINT_URL + globals['token'], - data: payloadString + data: payloadString, }; }, /** * 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. */ @@ -471,19 +568,26 @@ export const spec = { } 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}` } ]; - } else { - return [ - { - type: 'image', - url: `${COOKY_SYNC_URL}?${syncParamUrl}` - } - ]; } }, diff --git a/modules/mediaimpactBidAdapter.js b/modules/mediaimpactBidAdapter.js new file mode 100644 index 00000000000..9a86632f052 --- /dev/null +++ b/modules/mediaimpactBidAdapter.js @@ -0,0 +1,211 @@ +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 bidRequestObject = { + adUnitCode: validBidRequest.adUnitCode, + sizes: validBidRequest.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(validBidRequest.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..2fc6697fffb --- /dev/null +++ b/modules/mediaimpactBidAdapter.md @@ -0,0 +1,44 @@ +# 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 + } + } + ] + } + ]; +``` 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 041db71cd34..6a8a35dbfd4 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -19,6 +19,12 @@ 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'; diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js index aee5f6230b2..5cf0ceaba18 100644 --- a/modules/mediasniperBidAdapter.js +++ b/modules/mediasniperBidAdapter.js @@ -241,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 fb580d81b94..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/' @@ -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); @@ -125,6 +137,7 @@ export const spec = { 'advertiserDomains': value['adomain'] } }; + 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) { diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 1e158236deb..fb3990e97f1 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -21,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/mgidXBidAdapter.js b/modules/mgidXBidAdapter.js index 7e084977cf1..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'; @@ -181,9 +181,18 @@ export const spec = { 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 a4158e2abfa..81200f28a6f 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -460,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/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 080ed2a556f..99cad1c7bc6 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -1,6 +1,7 @@ import { buildUrl, formatQS, + generateUUID, isFn, logInfo, safeJSONParse, @@ -11,12 +12,20 @@ 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) { @@ -72,6 +81,7 @@ export const spec = { return validBidRequests.map((bidRequest) => { const payload = { adunit: bidRequest.adUnitCode, + ik: window.msna_ik, request_id: bidRequest.bidId, timeout: bidderRequest.timeout, }; 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/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 5dea4f9b651..de91b508125 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -8,7 +8,6 @@ import { getWindowTop, isArray, isStr, - logMessage, parseGPTSingleSizeArrayToRtbSize, parseUrl, triggerPixel, @@ -18,7 +17,6 @@ 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'; @@ -31,35 +29,44 @@ const TEST_ENDPOINT = 'https://test.pbs.nextmillmedia.com/openrtb2/auction'; 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 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, +}; -const VIDEO_PARAMS = [ - 'api', - 'linearity', - 'maxduration', - 'mimes', - 'minduration', - 'placement', - 'playbackmethod', - 'protocols', - 'startdelay', -]; - +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', ]; -const sendingDataStatistic = initSendingDataStatistic(); -events.on(CONSTANTS.EVENTS.AUCTION_INIT, auctionInitHandler); - -const EXPIRENCE_WURL = 20 * 60000; -const wurlMap = {}; -cleanWurl(); - -events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -75,7 +82,7 @@ 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; @@ -83,9 +90,11 @@ export const spec = { const site = getSiteObj(); const device = getDeviceObj(); + const {cur, mediaTypes} = getCurrency(bid); const postBody = { id: bidderRequest?.bidderRequestId, + cur, ext: { prebid: { storedrequest: { @@ -107,7 +116,7 @@ export const spec = { imp: [], }; - postBody.imp.push(getImp(bid, id)); + postBody.imp.push(getImp(bid, id, mediaTypes)); setConsentStrings(postBody, bidderRequest); setOrtb2Parameters(postBody, bidderRequest?.ortb2); setEids(postBody, bid); @@ -129,6 +138,8 @@ export const spec = { params, auctionId, }); + + this.getUrlPixelMetric(CONSTANTS.EVENTS.BID_REQUESTED, bid); }); return requests; @@ -142,11 +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); @@ -157,7 +163,7 @@ 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: { @@ -176,6 +182,8 @@ export const spec = { }; bidResponses.push(bidResponse); + + this.getUrlPixelMetric(CONSTANTS.EVENTS.BID_RESPONSE, bid); }); }); @@ -197,7 +205,7 @@ export const spec = { responses.forEach(response => { if (syncOptions.pixelEnabled) setPixelImages(response); if (syncOptions.iframeEnabled) setPixelIframes(response); - }) + }); } if (!pixels.length) { @@ -209,6 +217,16 @@ export const spec = { }, 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; @@ -242,9 +260,16 @@ export const spec = { return url; }, + + onTimeout(bids) { + for (const bid of bids) { + this.getUrlPixelMetric(CONSTANTS.EVENTS.BID_TIMEOUT, bid); + }; + }, }; -export function getImp(bid, id) { +export function getImp(bid, id, mediaTypes) { + const {banner, video} = mediaTypes; const imp = { id: bid.adUnitCode, ext: { @@ -256,25 +281,46 @@ export function getImp(bid, id) { }, }; - const banner = deepAccess(bid, 'mediaTypes.banner'); - if (banner) { - imp.banner = { - format: (banner?.sizes || []).map(s => { return {w: s[0], h: s[1]} }), - }; - }; + getImpBanner(imp, banner); + getImpVideo(imp, video); - 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) || {}); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; - }; + 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, }; +}; - return imp; +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) { @@ -334,7 +380,36 @@ export function replaceUsersyncMacros(url, gdprConsent = {}, uspConsent = '', gp .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... @@ -447,126 +522,4 @@ function 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 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 new SendingDataStatistic(); -} - registerBidder(spec); diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index eab174d22dd..8a41efe4dcc 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -14,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 c65544936fa..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 = '3.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. */ @@ -199,7 +207,7 @@ function interpretResponse(serverResponse) { response.adUrl = bid.ext.adUrl; } } - if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.ext.vastXml; + 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 index 3a7912c37e1..3a272c3f796 100644 --- a/modules/nobidAnalyticsAdapter.js +++ b/modules/nobidAnalyticsAdapter.js @@ -6,7 +6,7 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; -const VERSION = '1.1.0'; +const VERSION = '2.0.1'; const MODULE_NAME = 'nobidAnalyticsAdapter'; const ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS = 5 * 1000; const RETENTION_SECONDS = 1 * 24 * 3600; @@ -49,11 +49,12 @@ function sendEvent (event, eventType) { return ret; } if (!nobidAnalytics.initOptions || !nobidAnalytics.initOptions.siteId || !event) return; - if (nobidAnalytics.isAnalyticsDisabled()) { + 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) { @@ -83,7 +84,7 @@ function cleanupObjectAttributes (obj, attributes) { } function sendBidWonEvent (event, eventType) { const data = deepClone(event); - cleanupObjectAttributes(data, ['bidderCode', 'size', 'statusMessage', 'adId', 'requestId', 'mediaType', 'adUnitCode', 'cpm', 'timeToRespond']); + cleanupObjectAttributes(data, ['bidderCode', 'size', 'statusMessage', 'adId', 'requestId', 'mediaType', 'adUnitCode', 'cpm', 'currency', 'originalCpm', 'originalCurrency', 'timeToRespond']); if (nobidAnalytics.topLocation) data.topLocation = nobidAnalytics.topLocation; sendEvent(data, eventType); } @@ -95,10 +96,18 @@ function sendAuctionEndEvent (event, eventType) { 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']); + 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) cleanupObjectAttributes(data.bidderRequests.bids, ['mediaTypes', 'adUnitCode', 'sizes', 'bidId']); - if (data.bidderRequests) cleanupObjectAttributes(data.bidderRequests.refererInfo, ['topmostLocation']); + 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) { @@ -147,12 +156,18 @@ nobidAnalytics = { isExpired (data) { return isExpired(data, this.retentionSeconds); }, - isAnalyticsDisabled () { + 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; - return stored.disabled; + 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; diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 77de3c6d97b..28fb38e14e5 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -5,6 +5,15 @@ import { BANNER, VIDEO } from '../src/mediaTypes.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').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'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); 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/onetagBidAdapter.js b/modules/onetagBidAdapter.js index ee6d2980385..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) { @@ -122,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) { diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 547447039da..39bd50f61a9 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,249 +1,487 @@ -import {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'; -import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.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 0f8bee213f7..a99bd1c5325 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -107,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 { diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 131ba0bc1f2..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/'; 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 152876b8d5d..27b858c84fe 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -3,6 +3,15 @@ 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'; diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index efc2effdd62..0f912384db7 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -5,6 +5,10 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { getGlobal } from '../src/prebidGlobal.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const storageManager = getStorageManager({ bidderCode: 'orbidder' }); /** 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/pangleBidAdapter.js b/modules/pangleBidAdapter.js index f4a52168743..c22a44687a2 100644 --- a/modules/pangleBidAdapter.js +++ b/modules/pangleBidAdapter.js @@ -1,4 +1,3 @@ -// ver V1.0.4 import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -155,15 +154,16 @@ export const spec = { }, buildRequests(bidRequests, bidderRequest) { + const reqArr = []; const videoBids = bidRequests.filter((bid) => isVideoBid(bid)); const bannerBids = bidRequests.filter((bid) => isBannerBid(bid)); - let requests = bannerBids.length - ? [createRequest(bannerBids, bidderRequest, BANNER)] - : []; + bannerBids.forEach((bid) => { + reqArr.push(createRequest([bid], bidderRequest, BANNER)); + }) videoBids.forEach((bid) => { - requests.push(createRequest([bid], bidderRequest, VIDEO)); + reqArr.push(createRequest([bid], bidderRequest, VIDEO)); }); - return requests; + return reqArr; }, interpretResponse(response, request) { diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js index b38131c1c18..f3062fa4ff0 100644 --- a/modules/pgamsspBidAdapter.js +++ b/modules/pgamsspBidAdapter.js @@ -40,7 +40,8 @@ function getPlacementReqData(bid) { }; if (bid.userId) { - getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.uid2?.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.id5id?.uid, 'id5-sync.com'); } if (placementId) { diff --git a/modules/pilotxBidAdapter.js b/modules/pilotxBidAdapter.js index bd3612d6429..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 = { @@ -37,7 +45,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) { @@ -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/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 5ae9a9fc27a..6e4aec8ad92 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -478,13 +478,13 @@ export function PrebidServer() { adapterMetrics }); } - done(); + done(false); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, onError(msg, error) { logError(`Prebid server call failed: '${msg}'`, error); bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, {error, bidderRequest})); - done(); + done(error.timedOut); }, onBid: function ({adUnit, bid}) { const metrics = bid.metrics = s2sBidRequest.metrics.fork().renameWith(); diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 7aeb4302280..1dd1532f423 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -118,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 diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 9c3869c480a..70a0f9b9a14 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -94,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; } @@ -111,7 +111,7 @@ 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) } @@ -365,6 +365,7 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { noFloorSignaled: isNoFloorSignaled, skipped: floorData.skipped, skipRate: deepAccess(floorData, 'data.skipRate') ?? floorData.skipRate, + skippedReason: floorData.skippedReason, floorMin: floorData.floorMin, modelVersion: deepAccess(floorData, 'data.modelVersion'), modelWeight: deepAccess(floorData, 'data.modelWeight'), @@ -411,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') || (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; @@ -445,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 received do not match allowed fields`); return false; } diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index 931c5e7d7e6..b42c4b8af3f 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -4,6 +4,15 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.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'; const CACHE_URL = 'https://prisma.nexx360.io/cache'; @@ -55,7 +64,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) { @@ -180,7 +189,7 @@ 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 + * @param {Bid} bid the bid that won the auction */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid 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/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 ad2a06ea86d..ced47086f7b 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -151,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', @@ -278,6 +279,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 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': adapterName, 'bc': bid.bidderCode || bid.bidder, @@ -303,7 +305,8 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, 'frv': bid.bidResponse ? bid.bidResponse.floorData?.floorRuleValue : undefined, - 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined + 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined, + 'pb': pg || undefined }); }); return partnerBids; @@ -391,7 +394,7 @@ 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)), @@ -450,14 +453,16 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { 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); @@ -466,7 +471,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { 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); @@ -474,6 +479,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { 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' + diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 192c657d976..f28feaa534d 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -7,6 +7,12 @@ 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: '; const ENDPOINT = 'https://hbopenbid.pubmatic.com/translator?source=prebid-client'; @@ -1004,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 = { @@ -1064,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) => { @@ -1211,6 +1221,11 @@ 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 @@ -1390,6 +1405,7 @@ export const spec = { } 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/pxyzBidAdapter.js b/modules/pxyzBidAdapter.js index 1ab432496a3..8b9dbea339b 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'; 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/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 4e93f2aa8eb..74abd0fb4a1 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,8 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } 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'; +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'; @@ -56,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) => { @@ -102,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; }; @@ -160,9 +298,24 @@ const parseAuctionConfigs = (serverResponse, bidRequest) => { } } +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') { @@ -183,7 +336,8 @@ export const spec = { bidId: bid.bidId, sizes: getAdUnitSizes(bid), params: bid.params, - fledgeEnabled: fledgeEligible + fledgeEnabled: fledgeEligible, + mediaType: (bid.mediaTypes && bid.mediaTypes.banner) ? 'display' : NATIVE })); return [{ @@ -195,9 +349,11 @@ export const spec = { interpretResponse: function (serverResponse, bidRequest) { const response = serverResponse.body; - const fledgeAuctionConfigs = parseAuctionConfigs(serverResponse, bidRequest); - const bids = (!response || !response.ads || response.ads.length === 0) ? [] : response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); + 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. 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/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index 2bac6c6dcba..1fe4b4d750c 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -178,17 +178,11 @@ export const spec = { return bidResponses; }, getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { - const syncs = []; - + let syncs = []; if (!serverResponses.length || !serverResponses[0].body) { return syncs; } - let pixels = serverResponses[0].body.pixels; - if (!pixels || !pixels.length) { - return syncs; - } - let gdprParams = ''; if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { @@ -200,14 +194,20 @@ export const spec = { } } - if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { - return [ - { - type: 'iframe', - url: 'https://media.reset-digital.com/prebid/async_usersync.html?' + gdprParams.length ? gdprParams : '', - }, - ]; + 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; }, }; 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/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 36513aeda47..b63e31266fb 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.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 @@ -346,7 +371,7 @@ function raiGetTimeoutURL(data) { url = url.replace('[timeout_publisher]', timeout) url = url.replace('[placement_hash]', params[0].pid) - if (REFERER != null) { + 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 78854129f8a..82790805303 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -470,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/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 5f94ec3dea4..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 @@ -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 8968d4c795a..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 diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 9882eb23ac3..c03065cd5a5 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -23,6 +23,10 @@ import { } 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'; @@ -197,9 +201,6 @@ 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 && (imp.video.pos = 1); bidRequest.params.position === 'btf' && imp.video && (imp.video.pos = 3); delete imp.ext?.prebid?.storedrequest; @@ -233,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 }); @@ -528,6 +529,8 @@ export const spec = { 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 => { @@ -679,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 @@ -693,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]; } @@ -821,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, @@ -829,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 }); }); } @@ -902,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) { @@ -957,6 +972,26 @@ 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']; @@ -1001,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 {*} diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index 51c326c2954..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'; 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/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 53cb67c4e6d..2264bc37ebb 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; +import { deepAccess, generateUUID, inIframe, mergeDeep } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -92,6 +92,10 @@ export const sharethroughAdapterSpec = { 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; + } + const imps = bidRequests .map((bidReq) => { const impression = { ext: {} }; @@ -104,6 +108,10 @@ export const sharethroughAdapterSpec = { 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 + } + if (videoRequest) { // default playerSize, only change this if we know width and height are properly defined in the request let [w, h] = [640, 360]; @@ -116,6 +124,14 @@ export const sharethroughAdapterSpec = { [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, @@ -132,7 +148,8 @@ export const sharethroughAdapterSpec = { startdelay: nullish(videoRequest.startdelay, 0), skipmin: nullish(videoRequest.skipmin, 0), skipafter: nullish(videoRequest.skipafter, 0), - placement: videoRequest.context === 'instream' ? 1 : +deepAccess(videoRequest, 'placement', 4), + placement: getVideoPlacementValue(videoRequest), + plcmt: videoRequest.plcmt ? videoRequest.plcmt : null, }; if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; @@ -179,7 +196,9 @@ export const sharethroughAdapterSpec = { 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, @@ -219,6 +238,15 @@ export const sharethroughAdapterSpec = { return response; }); + + if (fledgeAuctionEnabled) { + return { + bids: bidsFromExchange, + fledgeAuctionConfigs: body.ext?.auctionConfigs || {}, + }; + } else { + return bidsFromExchange; + } }, getUserSyncs: (syncOptions, serverResponses) => { 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/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index 9cdb85d4f25..250c1ebb19e 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -5,6 +5,13 @@ import { 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 = { diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index b735953d099..ac0422842d5 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -8,6 +8,14 @@ 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'; const SMAATO_ENDPOINT = 'https://prebid.ad.smaato.net/oapi/prebid'; diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 313d466bd2e..7edaaa36957 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -3,6 +3,12 @@ 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; @@ -126,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 45cc45192ef..8394814365c 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -21,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/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 2409bebbc59..6920983e50d 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -41,8 +41,16 @@ function getAdUrlByRegion(bid) { try { const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const region = timezone.split('/')[0]; - if (region === 'Europe') adUrl = adUrls['EU']; - else adUrl = adUrls['US_EAST']; + + 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']; } @@ -74,6 +82,7 @@ export const spec = { location = winTop.location; logMessage(e); }; + let placements = []; let request = { 'deviceWidth': winTop.screen.width, @@ -83,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) { @@ -103,8 +114,10 @@ export const spec = { for (let i = 0; i < len; i++) { let bid = validBidRequests[i]; + if (i === 0) adUrl = getAdUrlByRegion(bid); - let traff = bid.params.traffic || BANNER + + let traff = bid.params.traffic || BANNER; placements.push({ placementId: bid.params.sourceid, bidId: bid.bidId, diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index 09c39e52825..7d4a4bca615 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -1,35 +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', - aliases: ['smile', 'sw'], + code: BIDDER_CODE, gvlid: GVL_ID, - supportedMediaTypes: [BANNER, VIDEO], + aliases: ['smile', 'sw'], + 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, @@ -60,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', @@ -85,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, @@ -108,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) { @@ -123,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); @@ -131,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 = ''; @@ -172,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 6d32d8f97a2..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, inIframe, getDNT} 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,9 +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, @@ -33,6 +39,11 @@ export const spec = { 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(), version: getGlobal().version, @@ -104,9 +115,7 @@ export const spec = { registerBidder(spec); function getPage(bidderRequest) { - return ( - getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.page') || window.location.href - ); + return getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.page') || window.location.href; } function getEndpoint() { @@ -193,6 +202,19 @@ 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; + } +} + function getSyncUrl(responses) { return getConfig(`${BIDDER_CODE}.syncUrl`) || deepAccess(responses[0], 'body.syncUrl'); } @@ -202,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/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 2c84854e507..1ce7665ddfc 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -7,6 +7,12 @@ 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(); 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 0ff5d842135..64604618680 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -15,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), @@ -103,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')) { @@ -328,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 index bfb527d46f2..0bccc1ec140 100644 --- a/modules/sparteoBidAdapter.js +++ b/modules/sparteoBidAdapter.js @@ -3,6 +3,10 @@ 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; diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 017544cc596..c1f1c5159fc 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -21,6 +21,12 @@ 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/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index c351b76d7ea..08b25abee01 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -12,7 +12,7 @@ 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.92'; +const BIDDER_VERSION = '5.93'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; @@ -77,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) => { @@ -216,8 +216,11 @@ const applyTopics = (validBidRequest, ortbRequest) => { }; 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 }; } @@ -243,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(); @@ -570,6 +573,7 @@ const parseNative = (nativeData, adUnitCode) => { } const renderCreative = (site, auctionId, bid, seat, request) => { + const { adLabel, id, slot, sn, page, publisherId, ref } = site; let gam; const mcad = { @@ -619,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/fpd/enrichment.js b/src/fpd/enrichment.js index 911509455e0..49e2f7d7cad 100644 --- a/src/fpd/enrichment.js +++ b/src/fpd/enrichment.js @@ -114,13 +114,19 @@ const ENRICHMENTS = { 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/native.js b/src/native.js index c4709dd77e2..1b6e13c77fc 100644 --- a/src/native.js +++ b/src/native.js @@ -1,6 +1,6 @@ import { deepAccess, - deepClone, + deepClone, getDefinedParams, insertHtmlIntoIframe, isArray, isBoolean, @@ -16,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( @@ -99,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 @@ -327,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 @@ -335,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, @@ -384,41 +407,40 @@ 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])); diff --git a/src/prebid.js b/src/prebid.js index df224ab83cf..750a4bdee1a 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -40,7 +40,7 @@ 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 '../libraries/creativeRender/direct.js'; +import {renderAdDirect} from './adRendering.js'; import {getHighestCpm} from './utils/reducers.js'; import {fillVideoDefaults} from './video.js'; @@ -552,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 @@ -567,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(); @@ -594,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 0ea93e7e4fb..1880f56f474 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,25 +4,26 @@ */ import * as events from './events.js'; -import {fireNativeTrackers, getAllAssetsMessage, getAssetMessage} from './native.js'; -import constants from './constants.json'; +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 {emitAdRenderFail, emitAdRenderSucceeded, handleRender} from './adRendering.js'; -import {PREBID_EVENT, PREBID_NATIVE, PREBID_REQUEST, PREBID_RESPONSE} from '../libraries/creativeRender/constants.js'; +import {handleCreativeEvent, handleNativeMessage, handleRender} from './adRendering.js'; +import {getCreativeRendererSource} from './creativeRenderers.js'; -const BID_WON = constants.EVENTS.BID_WON; -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, }); } @@ -67,13 +68,24 @@ export function receiveMessage(ev) { } } +function getResizer(bidResponse) { + return function (width, height) { + resizeRemoteCreative({...bidResponse, width, height}); + } +} function handleRenderRequest(reply, message, bidResponse) { - handleRender(function (adData) { - resizeRemoteCreative(bidResponse); - reply(Object.assign({ - message: PREBID_RESPONSE, - }, adData)); - }, {options: message.options, adId: message.adId, 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) { @@ -87,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); } @@ -100,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)}) } } @@ -115,40 +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 resizeRemoteCreative({adId, adUnitCode, width, height}) { + function getDimension(value) { + return value ? value + 'px' : '100%'; + } // 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.`); } 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/utils.js b/src/utils.js index 67ed7339675..c7ce5f22f9a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -252,25 +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'; - f.style.height = '0px'; - f.style.width = '0px'; - f.allowtransparency = 'true'; - return f; + return createIframe(document, { + id: getUniqueIdentifierStr(), + width: 0, + height: 0, + src: 'about:blank' + }, { + display: 'none', + height: '0px', + width: '0px', + border: '0px' + }); } /* @@ -898,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; diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js index 392ed1c9ad7..0972d175848 100644 --- a/src/utils/ttlCollection.js +++ b/src/utils/ttlCollection.js @@ -1,5 +1,5 @@ import {GreedyPromise} from './promise.js'; -import {binarySearch, timestamp} from '../utils.js'; +import {binarySearch, logError, timestamp} from '../utils.js'; /** * Create a set-like collection that automatically forgets items after a certain time. @@ -27,6 +27,7 @@ export function ttlCollection( } = {} ) { const items = new Map(); + const callbacks = []; const pendingPurge = []; const markForPurge = monotonic ? (entry) => pendingPurge.push(entry) @@ -43,6 +44,13 @@ export function ttlCollection( 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++; } @@ -135,5 +143,20 @@ export function ttlCollection( 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/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/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/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 137d009bd18..06d3d538596 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -25,6 +25,11 @@ 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'); /* use this method to test individual files instead of the whole prebid.js project */ @@ -57,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, @@ -114,6 +120,7 @@ function mockBidRequest(bid, opts) { }, 'adUnitCode': adUnitCode || ADUNIT_CODE, 'transactionId': bid.transactionId, + adUnitId: bid.adUnitId, 'sizes': [[300, 250], [300, 600]], 'bidId': bid.requestId, 'bidderRequestId': requestId, @@ -791,7 +798,7 @@ describe('auctionmanager.js', function () { }); adUnits = [{ code: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE}, ] @@ -849,11 +856,11 @@ describe('auctionmanager.js', function () { bids = [ { adUnitCode: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, ttl: 10 }, { adUnitCode: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, ttl: 100 } ]; @@ -882,7 +889,7 @@ describe('auctionmanager.js', function () { 'bids': { bd: [{ adUnitCode: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, ttl: 10 }], entries: () => auctionManager.getBidsReceived() @@ -933,7 +940,7 @@ describe('auctionmanager.js', function () { } }, code: ADUNIT_CODE, - transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ] @@ -1134,7 +1141,7 @@ describe('auctionmanager.js', function () { }, bidRequests[0].bids[0]); Object.assign(bidRequests[0].bids[0], { adUnitCode: ADUNIT_CODE1, - transactionId: ADUNIT_CODE1, + adUnitId: ADUNIT_CODE1, }); makeRequestsStub.returns(bidRequests); @@ -1186,6 +1193,7 @@ describe('auctionmanager.js', function () { adUnits = [{ code: ADUNIT_CODE, transactionId: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, @@ -1357,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'}}, ] @@ -1570,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'}}, ], @@ -1702,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, @@ -1773,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 }), diff --git a/test/spec/creative/crossDomainCreative_spec.js b/test/spec/creative/crossDomainCreative_spec.js index 765d5e5311a..f4c98aa7b50 100644 --- a/test/spec/creative/crossDomainCreative_spec.js +++ b/test/spec/creative/crossDomainCreative_spec.js @@ -1,17 +1,18 @@ -import {renderer} from '../../../libraries/creativeRender/crossDomain.js'; +import {renderer} from '../../../creative/crossDomain.js'; import { - AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, EXCEPTION, NO_AD, - PREBID_EVENT, - PREBID_REQUEST, - PREBID_RESPONSE -} from '../../../libraries/creativeRender/constants.js'; + ERROR_EXCEPTION, + EVENT_AD_RENDER_FAILED, EVENT_AD_RENDER_SUCCEEDED, + MESSAGE_EVENT, + MESSAGE_REQUEST, + MESSAGE_RESPONSE +} from '../../../creative/constants.js'; describe('cross-domain creative', () => { - let win, renderAd, messages, mkIframe, listeners; + const ORIGIN = 'https://example.com'; + let win, renderAd, messages, mkIframe; beforeEach(() => { messages = []; - listeners = []; mkIframe = sinon.stub(); win = { document: { @@ -28,7 +29,6 @@ describe('cross-domain creative', () => { } }) }, - addEventListener: sinon.stub().callsFake((_, listener) => listeners.push(listener)), parent: { postMessage: sinon.stub().callsFake((payload, targetOrigin, transfer) => { messages.push({payload: JSON.parse(payload), targetOrigin, transfer}); @@ -44,138 +44,150 @@ describe('cross-domain creative', () => { }); it('generates request message with adId and clickUrl', () => { - renderAd({adId: '123', clickUrl: 'https://click-url.com'}); + renderAd({adId: '123', clickUrl: 'https://click-url.com', pubUrl: ORIGIN}); expect(messages[0].payload).to.eql({ - message: PREBID_REQUEST, + 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); }) - Object.entries({ - 'MessageChannel': (msg) => messages[0].transfer[0].postMessage(msg), - 'message listener': (msg) => listeners.forEach((fn) => fn({data: msg})) - }).forEach(([t, transport]) => { - describe(`when using ${t}`, () => { - function reply(msg) { - transport(JSON.stringify(msg)) - }; + 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'}); - reply({adId: '123', ad: 'markup'}); - sinon.assert.notCalled(mkIframe); - }) + it('ignores messages that are not a prebid response message', () => { + renderAd({adId: '123', pubUrl: ORIGIN}); + reply({adId: '123', ad: 'markup'}); + sinon.assert.notCalled(mkIframe); + }) - describe('signals AD_RENDER_FAILED', () => { - it('on exception', (done) => { - mkIframe.callsFake(() => { throw new Error('error message') }); - renderAd({adId: '123'}); - reply({message: PREBID_RESPONSE, adId: '123', ad: 'markup'}); - setTimeout(() => { - expect(messages[1].payload).to.eql({ - message: PREBID_EVENT, - adId: '123', - event: AD_RENDER_FAILED, - info: { - reason: EXCEPTION, - message: 'error message' - } - }) - done(); - }, 100) - }); + 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('on missing ad', (done) => { - renderAd({adId: '123'}); - reply({message: PREBID_RESPONSE, adId: '123'}); + 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, { - message: PREBID_EVENT, adId: '123', - event: AD_RENDER_FAILED, + message: MESSAGE_EVENT, + event: EVENT_AD_RENDER_FAILED, info: { - reason: NO_AD, + reason, + message: sinon.match(val => message == null || message === val) } - }) + }); done(); }, 100) }) }); - describe('rendering', () => { - let iframe; - - beforeEach(() => { - iframe = { - attrs: {}, - setAttribute: sinon.stub().callsFake((k, v) => iframe.attrs[k.toLowerCase()] = v), - contentDocument: { - open: sinon.stub(), - write: sinon.stub(), - close: sinon.stub(), - } - } - mkIframe.callsFake(() => iframe); + 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 })}' }); - - it('renders adUrl as iframe src', (done) => { - renderAd({adId: '123'}); - reply({message: PREBID_RESPONSE, adId: '123', adUrl: 'some-url'}); + setTimeout(() => { + expect(messages[1]).to.not.exist; + window._resolve(); setTimeout(() => { - sinon.assert.calledWith(win.document.body.appendChild, iframe); - expect(iframe.attrs.src).to.eql('some-url'); + sinon.assert.match(messages[1].payload, { + adId: '123', + message: MESSAGE_EVENT, + event: EVENT_AD_RENDER_SUCCEEDED + }) + delete window._resolve; done(); }, 100) - }); + }, 100) + }) - it('renders ad through document.write', (done) => { - renderAd({adId: '123'}); - reply({message: PREBID_RESPONSE, adId: '123', ad: 'some-markup'}); + 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(() => { - sinon.assert.calledWith(win.document.body.appendChild, iframe); - sinon.assert.called(iframe.contentDocument.open); - sinon.assert.calledWith(iframe.contentDocument.write, 'some-markup'); - sinon.assert.called(iframe.contentDocument.close); - done(); + try { + sinon.assert.calledWith(window._reply, sinon.match({data: JSON.stringify('response')})); + done(); + } finally { + delete window._reply; + } }, 100) - }); - - Object.entries({ - adUrl: 'mock-ad-url', - ad: 'mock-ad-markup' - }).forEach(([prop, propValue]) => { - describe(`when message has ${prop}`, () => { - beforeEach((done) => { - renderAd({adId: '123'}); - reply({ - message: PREBID_RESPONSE, - adId: '123', - [prop]: propValue, - width: 100, - height: 200 - }); - setTimeout(done, 100); - }); - - it('emits AD_RENDER_SUCCEEDED', () => { - expect(messages[1].payload).to.eql({ - message: PREBID_EVENT, - adId: '123', - event: AD_RENDER_SUCCEEDED - }) - }); - - it('sets iframe height / width to ad height / width', () => { - sinon.assert.match(iframe.attrs, { - width: 100, - height: 200 - }) - }) - }) - }) + }, 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_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 40692360dca..80ee0dd6cd2 100644 --- a/test/spec/fpd/enrichment_spec.js +++ b/test/spec/fpd/enrichment_spec.js @@ -186,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 => { @@ -362,7 +377,7 @@ describe('FPD enrichment', () => { setup(); cdep = Promise.resolve('example-test-label'); return fpd().then(ortb2 => { - expect(ortb2.device.ext).to.not.exist; + expect(ortb2.device.ext?.cdep).to.not.exist; if (navigator.cookieDeprecationLabel) { sinon.assert.notCalled(navigator.cookieDeprecationLabel.getValue); } @@ -373,7 +388,7 @@ describe('FPD enrichment', () => { 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).to.not.exist; + expect(ortb2.device.ext?.cdep).to.not.exist; }) }); }); 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/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/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 744f3c69e83..13c02cc9bae 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -975,6 +975,41 @@ describe('Adagio bid adapter', () => { 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() { @@ -1129,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', @@ -1202,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 = { @@ -1379,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() { @@ -1497,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); @@ -1516,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 c1acff522c0..d4c5f5c3c38 100644 --- a/test/spec/modules/adfBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -142,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 () { @@ -1007,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 } } } @@ -1070,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 () { @@ -1260,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/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index b4d84634962..8730f2c1a0d 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -130,7 +130,7 @@ describe('admaticBidAdapter', () => { ], 'mediatype': {}, 'type': 'banner', - 'id': '2205da7a81846b', + 'id': 1, 'floors': { 'banner': { '300x250': { 'currency': 'USD', 'floor': 1 }, @@ -434,7 +434,7 @@ describe('admaticBidAdapter', () => { 'h': 90 } ], - 'id': '2205da7a81846b', + 'id': 1, 'mediatype': {}, 'type': 'banner', 'floors': { @@ -802,7 +802,10 @@ describe('admaticBidAdapter', () => { 'price': 0.01, 'type': 'banner', 'bidder': 'admatic', - 'mime_type': 'iframe', + 'mime_type': { + 'name': 'backfill', + 'force': false + }, 'adomain': ['admatic.com.tr'], 'party_tag': '
', 'iurl': 'https://www.admatic.com.tr' @@ -814,7 +817,10 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', - 'mime_type': 'iframe', + 'mime_type': { + 'name': 'backfill', + 'force': false + }, 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': '', @@ -822,25 +828,15 @@ describe('admaticBidAdapter', () => { }, { 'id': 3, - 'creative_id': '3741', - 'width': 300, - 'height': 250, - 'price': 0.01, - 'type': 'video', - 'mime_type': 'iframe', - 'bidder': 'admatic', - 'adomain': ['admatic.com.tr'], - 'party_tag': 'https://www.admatic.com.tr', - 'iurl': 'https://www.admatic.com.tr' - }, - { - 'id': 4, 'creative_id': '3742', 'width': 1, 'height': 1, 'price': 0.01, 'type': 'native', - 'mime_type': 'iframe', + 'mime_type': { + 'name': 'backfill', + 'force': false + }, 'bidder': 'admatic', 'adomain': ['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"]}}', @@ -863,7 +859,10 @@ describe('admaticBidAdapter', () => { ad: '
', creativeId: '374', meta: { - model: 'iframe', + model: { + 'name': 'backfill', + 'force': false + }, advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -877,11 +876,13 @@ describe('admaticBidAdapter', () => { currency: 'TRY', mediaType: 'video', netRevenue: true, - vastImpUrl: 'https://www.admatic.com.tr', vastXml: '', creativeId: '3741', meta: { - model: 'iframe', + model: { + 'name': 'backfill', + 'force': false + }, advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -890,24 +891,6 @@ describe('admaticBidAdapter', () => { { requestId: 3, cpm: 0.01, - width: 300, - height: 250, - currency: 'TRY', - mediaType: 'video', - netRevenue: true, - vastImpUrl: 'https://www.admatic.com.tr', - vastXml: 'https://www.admatic.com.tr', - creativeId: '3741', - meta: { - model: 'iframe', - advertiserDomains: ['admatic.com.tr'] - }, - ttl: 60, - bidder: 'admatic' - }, - { - requestId: 4, - cpm: 0.01, width: 1, height: 1, currency: 'TRY', @@ -933,7 +916,10 @@ describe('admaticBidAdapter', () => { }, creativeId: '3742', meta: { - model: 'iframe', + model: { + 'name': 'backfill', + 'force': false + }, advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -944,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/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index e109ca1829c..71f0a6a3a6c 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -660,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() { 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 0a2cd60d89e..7952f23189e 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -38,23 +38,23 @@ describe('AdqueryIdSystem', function () { const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.contains(`https://bidder.adquery.io/prebid/qid?qid=`); - request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: '6dd9eab7dfeab7df6dd9ea'})); - expect(callbackSpy.lastCall.lastArg).to.deep.equal('6dd9eab7dfeab7df6dd9ea'); + 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('allows configurable id url', function () { const config = { params: { - url: 'https://another_bidder.adquery.io/qid' + 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.contains('https://another_bidder.adquery.io/qid'); - request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: 'testqid'})); + 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('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 index 290a6822f69..a62dce8af97 100644 --- a/test/spec/modules/adstirBidAdapter_spec.js +++ b/test/spec/modules/adstirBidAdapter_spec.js @@ -166,6 +166,7 @@ describe('AdstirAdapter', function () { 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); diff --git a/test/spec/modules/ajaBidAdapter_spec.js b/test/spec/modules/ajaBidAdapter_spec.js index 3137c9dc24e..dbc72d113f4 100644 --- a/test/spec/modules/ajaBidAdapter_spec.js +++ b/test/spec/modules/ajaBidAdapter_spec.js @@ -73,9 +73,32 @@ describe('AjaAdapter', function () { 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: { @@ -87,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&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&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&'); + 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&`); }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 193e8aa64f8..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: { @@ -1204,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], @@ -1575,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': '', @@ -1623,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/automatadAnalyticsAdapter_spec.js b/test/spec/modules/automatadAnalyticsAdapter_spec.js index e591f7e8e95..a7dd28a8dc0 100644 --- a/test/spec/modules/automatadAnalyticsAdapter_spec.js +++ b/test/spec/modules/automatadAnalyticsAdapter_spec.js @@ -6,6 +6,18 @@ 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, @@ -117,20 +129,10 @@ describe('Automatad Analytics Adapter', () => { describe('Behaviour of the adapter when the sdk has loaded', () => { before(() => { spec.enableAnalytics(CONFIG_WITH_DEBUG); - const obj = { - auctionInitHandler: (args) => {}, - bidResponseHandler: (args) => {}, - bidderDoneHandler: (args) => {}, - bidWonHandler: (args) => {}, - noBidHandler: (args) => {}, - auctionDebugHandler: (args) => {}, - bidderTimeoutHandler: (args) => {}, - bidRequestedHandler: (args) => {}, - bidRejectedHandler: (args) => {} - } global.window.atmtdAnalytics = obj - + exports.qBeingUsed = false + exports.qTraversalComplete = undefined Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) }) beforeEach(() => { @@ -143,8 +145,12 @@ describe('Automatad Analytics Adapter', () => { 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', () => { @@ -298,6 +304,85 @@ describe('Automatad Analytics Adapter', () => { }); }); + 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({ @@ -312,6 +397,8 @@ describe('Automatad Analytics Adapter', () => { sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); }) + exports.queuePointer = 0; + exports.retryCount = 0; }) beforeEach(() => { sandbox = sinon.createSandbox(); @@ -326,7 +413,6 @@ describe('Automatad Analytics Adapter', () => { sandbox.restore(); exports.queuePointer = 0; exports.retryCount = 0; - exports.__atmtdAnalyticsQueue = [] spec.disableAnalytics(); }) @@ -437,7 +523,6 @@ describe('Automatad Analytics Adapter', () => { } }); sandbox = sinon.createSandbox(); - sandbox.reset() const obj = { auctionInitHandler: (args) => {}, bidResponseHandler: (args) => {}, @@ -473,8 +558,10 @@ describe('Automatad Analytics Adapter', () => { ['impressionViewable', {type: 'impressionViewable'}] ] }); - after(() => { + afterEach(() => { sandbox.restore(); + }) + after(() => { spec.disableAnalytics(); }) 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/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/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/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js index 78f6a9d410d..4d816c4e816 100644 --- a/test/spec/modules/connatixBidAdapter_spec.js +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -90,6 +90,10 @@ describe('connatixBidAdapter', function () { gdprApplies: true }, uspConsent: '1YYY', + gppConsent: { + gppString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + applicableSections: [7] + }, ortb2: { site: { data: { @@ -128,6 +132,7 @@ describe('connatixBidAdapter', function () { 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); }); }); diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 73d1978a9d9..9503a050092 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -465,7 +465,7 @@ describe('Conversant adapter tests', function() { before(() => { request = spec.buildRequests(bidRequests, {}); - response = spec.interpretResponse(bidResponses, request).bids; + response = spec.interpretResponse(bidResponses, request); }); it('Banner', function() { diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 5dc6d74d90e..1709acb465f 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -1110,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], } }; @@ -1120,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' @@ -1979,6 +2021,48 @@ 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.adrender).to.equal(1); + }); + it('should properly parse a bid response with a networkId with twin ad unit banner win', function () { const response = { body: { @@ -2454,46 +2538,102 @@ describe('The Criteo bidding adapter', function () { }); 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: { - igbid: [{ + igi: [{ impid: 'test-bidId', - igbuyer: [{ - origin: 'https://first-buyer-domain.com', - buyerdata: { - foo: 'bar', - }, - }, { - origin: 'https://second-buyer-domain.com', - buyerdata: { - foo: 'baz', - }, + igs: [{ + impid: 'test-bidId', + bidId: 'test-bidId', + config: auctionConfig1 }] }, { impid: 'test-bidId-2', - igbuyer: [{ - origin: 'https://first-buyer-domain.com', - buyerdata: { - foo: 'bar', - }, - }, { - origin: 'https://second-buyer-domain.com', - buyerdata: { - foo: 'baz', - }, + igs: [{ + impid: 'test-bidId-2', + bidId: 'test-bidId-2', + config: auctionConfig2 }] - }], - seller: 'https://seller-domain.com', - sellerTimeout: 500, - sellerSignals: { - foo: 'bar', - }, - sellerSignalsPerImp: { - 'test-bidId': { - foo2: 'bar2', - } - }, + }] }, }, }; @@ -2544,50 +2684,13 @@ describe('The Criteo bidding adapter', function () { expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(2); expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ bidId: 'test-bidId', - config: { - 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' - }, - }, - seller: 'https://seller-domain.com', - sellerTimeout: 500, - sellerSignals: { - foo: 'bar', - foo2: 'bar2', - floor: 1, - sellerCurrency: 'EUR', - }, - }, + impid: 'test-bidId', + config: auctionConfig1, }); expect(interpretedResponse.fledgeAuctionConfigs[1]).to.deep.equal({ bidId: 'test-bidId-2', - config: { - 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' - }, - }, - seller: 'https://seller-domain.com', - sellerTimeout: 500, - sellerSignals: { - foo: 'bar', - floor: 1, - sellerCurrency: 'EUR', - }, - }, + impid: 'test-bidId-2', + config: auctionConfig2, }); }); 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 4c12e9fa211..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,37 +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 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('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'); @@ -98,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'); }); @@ -120,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; @@ -290,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', @@ -639,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/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index 05216ff126c..f1475ec3739 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -1,5 +1,16 @@ import { expect } from 'chai'; -import { spec, getPmgUID, storage, getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink } from 'modules/discoveryBidAdapter.js'; +import { + spec, + getPmgUID, + storage, + getPageTitle, + getPageDescription, + getPageKeywords, + getConnectionDownLink, + THIRD_PARTY_COOKIE_ORIGIN, + COOKIE_KEY_MGUID, + getCurrentTimeToUTCString +} from 'modules/discoveryBidAdapter.js'; import * as utils from 'src/utils.js'; describe('discovery:BidAdapterTests', function () { @@ -77,9 +88,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({ @@ -93,11 +191,27 @@ 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() { @@ -128,7 +242,7 @@ describe('discovery:BidAdapterTests', function () { storage.getCookie.callsFake(() => 'existing-uuid'); const uid = getPmgUID(); expect(uid).to.equal('existing-uuid'); - expect(storage.setCookie.called).to.be.false; + expect(storage.setCookie.called).to.be.true; }); it('should not set new UUID when cookies are not enabled', () => { @@ -443,5 +557,60 @@ describe('discovery Bid Adapter Tests', function () { 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/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/eids_spec.js b/test/spec/modules/eids_spec.js index 25e70f12ced..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 = { 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/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 98770fa80bc..9ad2b69e89c 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -22,6 +22,7 @@ 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) => ({ @@ -30,12 +31,15 @@ const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ( 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 = (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() { @@ -146,5 +150,15 @@ describe('EUID module', function() { 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/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js index 60a8e196ae0..8ab11171121 100644 --- a/test/spec/modules/fledgeForGpt_spec.js +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -1,419 +1,177 @@ -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 {onAuctionConfigFactory, setPAAPIConfigFactory, slotConfigurator} from 'modules/fledgeForGpt.js'; import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; -import {hook} from '../../../src/hook.js'; import 'modules/appnexusBidAdapter.js'; import 'modules/rubiconBidAdapter.js'; -import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; -import * as events from 'src/events.js'; -import CONSTANTS from 'src/constants.json'; -import {getGlobal} from '../../../src/prebidGlobal.js'; +import {deepSetValue} from '../../../src/utils.js'; +import {config} from 'src/config.js'; describe('fledgeForGpt module', () => { - let sandbox; + let sandbox, fledgeAuctionConfig; beforeEach(() => { sandbox = sinon.sandbox.create(); + fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; }); afterEach(() => { sandbox.restore(); }); - describe('addComponentAuction', function () { - before(() => { - fledge.init({enabled: true}); - }); - const fledgeAuctionConfig = { - seller: 'bidder', - mock: 'config' - }; - - describe('addComponentAuctionHook', function () { - let nextFnSpy, mockGptSlot; - beforeEach(function () { - nextFnSpy = sinon.spy(); - mockGptSlot = { - setConfig: sinon.stub(), - getAdUnitPath: () => 'mock/gpt/au' - }; - sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); - }); - - it('should call next()', function () { - const request = {auctionId: 'aid', adUnitCode: 'auc'}; - fledge.addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); - sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + 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, + }] }); + }); - it('should collect auction configs and route them to GPT at end of auction', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - const cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; - const cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1'}, cf1); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au2'}, cf2); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); - sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au1'); - sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au2'); + 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: 'b1', - auctionConfig: cf1, + 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: 'b2', - auctionConfig: cf2, + configKey: 's1', + auctionConfig: {seller: 's1'} + }, { + configKey: 's2', + auctionConfig: null }] - }); + }) }); - it('should drop auction configs after end of auction', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + 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 augment auctionSignals with FPD', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1', ortb2: {fpd: 1}, ortb2Imp: {fpd: 2}}, fledgeAuctionConfig); - events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + 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: 'bidder', - auctionConfig: { - ...fledgeAuctionConfig, - auctionSignals: { - prebid: { - ortb2: {fpd: 1}, - ortb2Imp: {fpd: 2} - } - } - }, + configKey: 's1', + auctionConfig: null + }, { + configKey: 's2', + auctionConfig: null }] - }) - }) - - 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'); - }); }); - + }) + }); + }); + describe('onAuctionConfig', () => { + [ + 'fledgeForGpt', + 'paapi.gpt' + ].forEach(namespace => { + describe(`using ${namespace} config`, () => { 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: 'aid'}; - setup(payload, values); - }); + '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 populate bidfloor/bidfloorcur', () => { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); - fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); - events.emit(CONSTANTS.EVENTS.AUCTION_END, payload); - sinon.assert.calledWith(mockGptSlot.setConfig, sinon.match(arg => { - return arg.componentAuction.some(au => au.auctionConfig.auctionSignals?.prebid?.bidfloor === bidfloor && au.auctionConfig.auctionSignals?.prebid?.bidfloorcur === bidfloorcur) - })) - }) - }); + 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('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(); + describe('setPAAPIConfigForGpt', () => { + let getPAAPIConfig, setGptConfig, setPAAPIConfigForGPT; + beforeEach(() => { + getPAAPIConfig = sinon.stub(); + setGptConfig = sinon.stub(); + setPAAPIConfigForGPT = setPAAPIConfigFactory(getPAAPIConfig, setGptConfig); }); - after(function () { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); - }); - - afterEach(function () { - config.resetConfig(); + 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); + }) }); - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] + 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'}] }, - }, - 'bids': [ - { - 'bidder': 'appnexus', + au2: { + componentAuctions: [{seller: 's3'}] }, - { - 'bidder': 'rubicon', - }, - ] - }]; - function expectFledgeFlags(...enableFlags) { - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() { - }, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.eql(enableFlags[0].enabled) - bidRequests[0].bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)) - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.eql(enableFlags[1].enabled) - bidRequests[1].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.setConfig({bidderSequence: 'fixed'}); - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - defaultForSlots: 1, - } - }); - 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', - fledgeForGpt: { - enabled: true, - bidders: ['appnexus'], - defaultForSlots: 1, - } - }); - expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); - }); - - it('should set fledgeEnabled correctly for all bidders', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - 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', - fledgeForGpt: { - 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) - ); + au3: 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({}); - }); + 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/gammaBidAdapter_spec.js b/test/spec/modules/gammaBidAdapter_spec.js index 35394df7d11..f3a28c08576 100644 --- a/test/spec/modules/gammaBidAdapter_spec.js +++ b/test/spec/modules/gammaBidAdapter_spec.js @@ -8,8 +8,9 @@ describe('gammaBidAdapter', function() { let bid = { 'bidder': 'gamma', 'params': { - siteId: '1465446377', - zoneId: '1515999290' + siteId: '1398219351', + zoneId: '1398219417', + region: 'SGP' }, 'adUnitCode': 'adunit-code', 'sizes': [ @@ -84,7 +85,7 @@ describe('gammaBidAdapter', function() { 'width': 300, 'height': 250, 'creativeId': '1515999070', - 'dealId': 'gax-paj2qarjf2g', + 'dealId': 'gax-lvpjgs5b9k4n', 'currency': 'USD', 'netRevenue': true, 'ttl': 300, diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 295b14dd796..4caf0276874 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -37,7 +37,6 @@ describe('gdpr enforcement', function () { let staticConfig = { cmpApi: 'static', timeout: 7500, - allowAuctionWithoutConsent: false, consentData: { getTCData: { 'tcString': 'COuqj-POu90rDBcBkBENAZCgAPzAAAPAACiQFwwBAABAA1ADEAbQC4YAYAAgAxAG0A', @@ -894,7 +893,6 @@ describe('gdpr enforcement', function () { setEnforcementConfig({ gdpr: { cmpApi: 'iab', - allowAuctionWithoutConsent: true, timeout: 5000 } }); diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index 30361ca5661..7b68b0dea46 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -404,16 +404,24 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { describe('isSampled', function() { it('should return true for invalid sampling rates', function() { - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', -1)).to.be.true; - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 1.2)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', -1, 0.0)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 1.2, 0.0)).to.be.true; }); it('should return determinist falsevalue for valid sampling rate given the predifined id and rate', function() { - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001)).to.be.false; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0)).to.be.false; }); it('should return determinist true value for valid sampling rate given the predifined id and rate', function() { - expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999, 0.0)).to.be.true; + }); + + it('should return determinist true value for valid sampling rate given the predifined id and rate when we split to non exploration first', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999, 0.0, 1.0)).to.be.true; + }); + + it('should return determinist false value for valid sampling rate given the predifined id and rate when we split to non exploration first', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001, 0.0, 1.0)).to.be.false; }); }); }); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 0611fa68bf8..abaa4b37fcd 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -1352,7 +1352,7 @@ describe('TheMediaGrid Adapter', function () { it('complicated case', function () { const fullResponse = [ - {'bid': [{'impid': '2164be6358b9', 'adid': '32_52_7543', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, + {'bid': [{'impid': '2164be6358b9', 'adid': '32_52_7543', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11, 'ext': {'dsa': {'adrender': 1}}}], 'seat': '1'}, {'bid': [{'impid': '4e111f1b66e4', 'adid': '32_54_4535', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, {'bid': [{'impid': '26d6f897b516', 'adid': '32_53_75467', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, {'bid': [{'impid': '326bde7fbf69', 'adid': '32_54_12342', 'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, @@ -1430,6 +1430,7 @@ describe('TheMediaGrid Adapter', function () { 'netRevenue': true, 'ttl': 360, 'meta': { + adrender: 1, advertiserDomains: [] }, }, diff --git a/test/spec/modules/growthCodeIdSystem_spec.js b/test/spec/modules/growthCodeIdSystem_spec.js index 97083047d4e..e3848dc4844 100644 --- a/test/spec/modules/growthCodeIdSystem_spec.js +++ b/test/spec/modules/growthCodeIdSystem_spec.js @@ -6,9 +6,12 @@ import {expect} from 'chai'; import {getStorageManager} from '../../../src/storageManager.js'; import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; -const GCID_EXPIRY = 45; const MODULE_NAME = 'growthCodeId'; -const SHAREDID = 'fe9c5c89-7d56-4666-976d-e07e73b3b664'; +const EIDS = '[{"source":"domain.com","uids":[{"id":"8212212191539393121","ext":{"stype":"ppuid"}}]}]'; +const GCID = 'e06e9e5a-273c-46f8-aace-6f62cf13ea71' + +const GCID_EID = '{"id": [{"source": "growthcode.io", "uids": [{"atype": 3,"id": "e06e9e5a-273c-46f8-aace-6f62cf13ea71"}]}]}' +const GCID_EID_EID = '{"id": [{"source": "growthcode.io", "uids": [{"atype": 3,"id": "e06e9e5a-273c-46f8-aace-6f62cf13ea71"}]},{"source": "domain.com", "uids": [{"id": "8212212191539393121", "ext": {"stype":"ppuid"}}]}]}' const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); @@ -23,11 +26,8 @@ describe('growthCodeIdSystem', () => { beforeEach(function () { logErrorStub = sinon.stub(utils, 'logError'); - storage.setDataInLocalStorage('_sharedid', SHAREDID); - const expiresStr = (new Date(Date.now() + (GCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); - if (storage.cookiesAreEnabled()) { - storage.setCookie('_sharedid', SHAREDID, expiresStr, 'LAX'); - } + storage.setDataInLocalStorage('gcid', GCID, null); + storage.setDataInLocalStorage('customerEids', EIDS, null); }); afterEach(function () { @@ -40,45 +40,33 @@ describe('growthCodeIdSystem', () => { }); }); - it('should NOT call the growthcode id endpoint if gdpr applies but consent string is missing', function () { - let submoduleCallback = growthCodeIdSubmodule.getId(getIdParams, { gdprApplies: true }, undefined); - expect(submoduleCallback).to.be.undefined; - }); - - it('should log an error if pid configParam was not passed when getId', function () { - growthCodeIdSubmodule.getId(); - expect(logErrorStub.callCount).to.be.equal(1); + it('test return of GCID', function () { + let ids; + ids = growthCodeIdSubmodule.getId(); + expect(ids).to.deep.equal(JSON.parse(GCID_EID)); }); - it('should log an error if sharedId (LocalStore) is not setup correctly', function () { - growthCodeIdSubmodule.getId({params: { - pid: 'TEST01', - publisher_id: '_sharedid_bad', - publisher_id_storage: 'html5', + it('test return of the GCID and an additional EID', function () { + let ids; + ids = growthCodeIdSubmodule.getId({params: { + customerEids: 'customerEids', }}); - expect(logErrorStub.callCount).to.be.equal(1); + expect(ids).to.deep.equal(JSON.parse(GCID_EID_EID)); }); - it('should log an error if sharedId (LocalStore) is not setup correctly', function () { - growthCodeIdSubmodule.getId({params: { - pid: 'TEST01', - publisher_id: '_sharedid_bad', - publisher_id_storage: 'cookie', + it('test return of the GCID and an additional EID (bad Local Store name)', function () { + let ids; + ids = growthCodeIdSubmodule.getId({params: { + customerEids: 'customerEidsBad', }}); - expect(logErrorStub.callCount).to.be.equal(1); + expect(ids).to.deep.equal(JSON.parse(GCID_EID)); }); - it('should call the growthcode id endpoint', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = growthCodeIdSubmodule.getId(getIdParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url.substr(0, 85)).to.be.eq('https://p2.gcprivacy.com/v1/pb?pid=TEST01&uid=' + SHAREDID + '&u='); - request.respond( - 200, - {}, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; + it('test decode function)', function () { + let ids; + ids = growthCodeIdSubmodule.decode(GCID, {params: { + customerEids: 'customerEids', + }}); + expect(ids).to.deep.equal(JSON.parse('{"growthCodeId":"' + GCID + '"}')); }); }) diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index e0529a895f5..52cfd0294e7 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -880,6 +880,19 @@ describe('gumgumAdapter', function () { expect(result.height = expectedSize[1]); }) + it('request size that matches response size for in-slot', function () { + const request = { ...bidRequest }; + const body = { ...serverResponse }; + const expectedSize = [[ 320, 50 ], [300, 600], [300, 250]]; + let result; + request.pi = 3; + body.ad.width = 300; + body.ad.height = 600; + result = spec.interpretResponse({ body }, request)[0]; + expect(result.width = expectedSize[1][0]); + expect(result.height = expectedSize[1][1]); + }) + it('defaults to use bidRequest sizes', function () { const { ad, jcsi, pag, thms, meta } = serverResponse const noAdSizes = { ...ad } diff --git a/test/spec/modules/hypelabBidAdapter_spec.js b/test/spec/modules/hypelabBidAdapter_spec.js index 4522073a2db..28d0739de79 100644 --- a/test/spec/modules/hypelabBidAdapter_spec.js +++ b/test/spec/modules/hypelabBidAdapter_spec.js @@ -92,7 +92,7 @@ const mockBidRequest = { placement_slug: 'test_placement', provider_version: '0.0.1', provider_name: 'prebid', - referrer: 'https://example.com', + location: 'https://example.com', sdk_version: '7.51.0-pre', sizes: [[728, 90]], wids: [], @@ -160,6 +160,9 @@ describe('hypelabBidAdapter', function () { expect(data.bidRequestsCount).to.be.a('number'); expect(data.bidderRequestsCount).to.be.a('number'); expect(data.bidderWinsCount).to.be.a('number'); + expect(data.dpr).to.be.a('number'); + expect(data.location).to.be.a('string'); + expect(data.floor).to.equal(null); }); describe('should set uuid to the first id in userIdAsEids', () => { @@ -211,6 +214,7 @@ describe('hypelabBidAdapter', function () { expect(data.ad).to.be.a('string'); expect(data.mediaType).to.be.a('string'); expect(data.meta.advertiserDomains).to.be.an('array'); + expect(data.meta.advertiserDomains[0]).to.be.a('string'); }); describe('should return a blank array if cpm is not set', () => { diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index ecce98d0b8d..af468f2fe4d 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1,5 +1,11 @@ import * as id5System from '../../../modules/id5IdSystem.js'; -import {coreStorage, getConsentHash, init, requestBidsHook, setSubmoduleRegistry} from '../../../modules/userId/index.js'; +import { + coreStorage, + getConsentHash, + init, + requestBidsHook, + setSubmoduleRegistry +} from '../../../modules/userId/index.js'; import {config} from '../../../src/config.js'; import * as events from '../../../src/events.js'; import CONSTANTS from '../../../src/constants.json'; @@ -10,7 +16,7 @@ import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {server} from '../../mocks/xhr.js'; import {expect} from 'chai'; -import { GreedyPromise } from '../../../src/utils/promise.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; const IdFetchFlow = id5System.IdFetchFlow; @@ -37,6 +43,22 @@ describe('ID5 ID System', function () { 'linkType': ID5_STORED_LINK_TYPE } }; + const EUID_STORED_ID = 'EUID_1'; + const EUID_SOURCE = 'uidapi.com'; + const ID5_STORED_OBJ_WITH_EUID = { + 'universal_uid': ID5_STORED_ID, + 'signature': ID5_STORED_SIGNATURE, + 'ext': { + 'linkType': ID5_STORED_LINK_TYPE, + 'euid': { + 'source': EUID_SOURCE, + 'uids': [{ + 'id': EUID_STORED_ID, + 'aType': 3 + }] + } + } + }; const ID5_RESPONSE_ID = 'newid5id'; const ID5_RESPONSE_SIGNATURE = 'abcdef'; const ID5_RESPONSE_LINK_TYPE = 2; @@ -886,6 +908,35 @@ describe('ID5 ID System', function () { }, {adUnits}); }); + it('should add stored EUID from cache to bids', function (done) { + id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_EUID), 1); + + init(config); + setSubmoduleRegistry([id5System.id5IdSubmodule]); + config.setConfig(getFetchLocalStorageConfig()); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property(`userId.euid`); + expect(bid.userId.euid.uid).is.equal(EUID_STORED_ID); + expect(bid.userIdAsEids[0].uids[0].id).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[1]).is.deep.equal({ + source: EUID_SOURCE, + uids: [{ + id: EUID_STORED_ID, + atype: 3, + ext: { + provider: ID5_SOURCE + } + }] + }) + }); + }); + done(); + }, {adUnits}); + }); + it('should add config value ID to bids', function (done) { init(config); setSubmoduleRegistry([id5System.id5IdSubmodule]); @@ -984,6 +1035,13 @@ describe('ID5 ID System', function () { it('should return undefined if passed a string', function () { expect(id5System.id5IdSubmodule.decode('somestring', getId5FetchConfig())).is.eq(undefined); }); + it('should decode euid from a stored object with EUID', function () { + expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_EUID, getId5FetchConfig()).euid).is.deep.equal({ + 'source': EUID_SOURCE, + 'uid': EUID_STORED_ID, + 'ext': {'provider': ID5_SOURCE} + }); + }); }); describe('A/B Testing', function () { diff --git a/test/spec/modules/innityBidAdapter_spec.js b/test/spec/modules/innityBidAdapter_spec.js index 192ab4911ee..820f535ba72 100644 --- a/test/spec/modules/innityBidAdapter_spec.js +++ b/test/spec/modules/innityBidAdapter_spec.js @@ -120,5 +120,11 @@ describe('innityAdapterTest', () => { expect(result[0].meta.advertiserDomains.length).to.equal(0); expect(result[0].meta.advertiserDomains).to.deep.equal([]); }); + + it('result with no bids', () => { + bidResponse.body = {}; + const result = spec.interpretResponse(bidResponse, bidRequest); + expect(result).to.deep.equal([]); + }); }); }); diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index 149983552f8..86f96834547 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -161,6 +161,19 @@ describe('InsticatorBidAdapter', function () { })).to.be.false; }); + it('should return true if video object is absent/undefined', () => { + expect(spec.isBidRequestValid({ + ...bidRequest, + ...{ + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + } + } + })).to.be.true; + }) + it('should return false if video placement is not a number', () => { expect(spec.isBidRequestValid({ ...bidRequest, diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 500f2239e55..7655868ffc3 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -625,6 +625,45 @@ describe('IndexexchangeAdapter', function () { ] }; + const DEFAULT_BANNER_BID_RESPONSE_WITH_DSA = { + cur: 'USD', + id: '11a22b33c44d', + seatbid: [ + { + bid: [ + { + crid: '12345', + adomain: ['www.abc.com'], + adid: '14851455', + impid: '1a2b3c4d', + cid: '3051266', + price: 100, + w: 300, + h: 250, + id: '1', + ext: { + dspid: 50, + pricelevel: '_100', + advbrandid: 303325, + advbrand: 'OECTA', + dsa: { + behalf: 'Advertiser', + paid: 'Advertiser', + transparency: [{ + domain: 'dsp1domain.com', + dsaparams: [1, 2] + }], + 'adrender': 1 + } + }, + adm: '' + } + ], + seat: '3970' + } + ] + }; + const DEFAULT_BANNER_BID_RESPONSE_WITHOUT_ADOMAIN = { cur: 'USD', id: '11a22b33c44d', @@ -919,6 +958,23 @@ describe('IndexexchangeAdapter', function () { const extractPayload = function (bidRequest) { return bidRequest.data } + const generateEid = function (numEid) { + const eids = []; + + for (let i = 1; i <= numEid; i++) { + const newEid = { + source: `eid_source_${i}.com`, + uids: [{ + id: `uid_id_${i}`, + }] + }; + + eids.push(newEid); + } + + return eids; + } + describe('inherited functions', function () { it('should exists and is a function', function () { const adapter = newBidder(spec); @@ -1485,6 +1541,17 @@ describe('IndexexchangeAdapter', function () { describe('buildRequestsUserId', function () { let validIdentityResponse; let validUserIdPayload; + const serverResponse = { + body: { + ext: { + pbjs_allow_all_eids: { + test: { + activated: false + } + } + } + } + }; beforeEach(function () { window.headertag = {}; @@ -1495,6 +1562,12 @@ describe('IndexexchangeAdapter', function () { afterEach(function () { delete window.headertag; + serverResponse.body.ext.features = { + pbjs_allow_all_eids: { + activated: false + } + }; + validIdentityResponse = {} }); it('IX adapter reads supported user modules from Prebid and adds it to Video', function () { @@ -1506,6 +1579,91 @@ describe('IndexexchangeAdapter', function () { expect(payload.user.eids).to.have.deep.members(DEFAULT_USERID_PAYLOAD); }); + it('IX adapter filters eids from prebid past the maximum eid limit', function () { + serverResponse.body.ext.features = { + pbjs_allow_all_eids: { + activated: true + } + }; + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); + let eid_sent_from_prebid = generateEid(55); + cloneValidBid[0].userIdAsEids = utils.deepClone(eid_sent_from_prebid); + const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; + const payload = extractPayload(request); + expect(payload.user.eids).to.have.lengthOf(50); + let eid_accepted = eid_sent_from_prebid.slice(0, 50); + expect(payload.user.eids).to.have.deep.members(eid_accepted); + expect(payload.ext.ixdiag.eidLength).to.equal(55); + }); + + it('IX adapter filters eids from IXL past the maximum eid limit', function () { + validIdentityResponse = { + MerkleIp: { + responsePending: false, + data: { + source: 'merkle.com', + uids: [{ + id: '1234-5678-9012-3456', + ext: { + keyID: '1234-5678', + enc: 1 + } + }] + } + }, + LiveIntentIp: { + responsePending: false, + data: { + source: 'liveintent.com', + uids: [{ + id: '1234-5678-9012-3456', + ext: { + keyID: '1234-5678', + rtiPartner: 'LDID', + enc: 1 + } + }] + } + } + }; + serverResponse.body.ext.features = { + pbjs_allow_all_eids: { + activated: true + } + }; + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); + let eid_sent_from_prebid = generateEid(49); + cloneValidBid[0].userIdAsEids = utils.deepClone(eid_sent_from_prebid); + const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; + const payload = extractPayload(request); + expect(payload.user.eids).to.have.lengthOf(50); + eid_sent_from_prebid.push({ + source: 'merkle.com', + uids: [{ + id: '1234-5678-9012-3456', + ext: { + keyID: '1234-5678', + enc: 1 + } + }] + }) + expect(payload.user.eids).to.have.deep.members(eid_sent_from_prebid); + expect(payload.ext.ixdiag.eidLength).to.equal(49); + }); + + it('All incoming eids are from unsupported source with feature toggle off', function () { + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); + let eid_sent_from_prebid = generateEid(20); + cloneValidBid[0].userIdAsEids = utils.deepClone(eid_sent_from_prebid); + const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; + const payload = extractPayload(request); + expect(payload.user.eids).to.be.undefined + expect(payload.ext.ixdiag.eidLength).to.equal(20); + }); + it('We continue to send in IXL identity info and Prebid takes precedence over IXL', function () { validIdentityResponse = { AdserverOrgIp: { @@ -1784,6 +1942,72 @@ describe('IndexexchangeAdapter', function () { expect(r.user.testProperty).to.be.undefined; }); + it('should set dsa field when defined', function () { + const dsa = { + dsarequired: 3, + pubrender: 0, + datatopub: 2, + transparency: [{ + domain: 'domain.com', + dsaparams: [1] + }] + } + const request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, { ortb2: {regs: { + ext: { + dsa: deepClone(dsa) + } + } + }})[0]; + const r = extractPayload(request); + + expect(r.regs.ext.dsa.dsarequired).to.equal(dsa.dsarequired); + expect(r.regs.ext.dsa.pubrender).to.equal(dsa.pubrender); + expect(r.regs.ext.dsa.datatopub).to.equal(dsa.datatopub); + expect(r.regs.ext.dsa.transparency).to.be.an('array'); + expect(r.regs.ext.dsa.transparency).to.have.deep.members(dsa.transparency); + }); + it('should not set dsa fields when fields arent appropriately defined', function () { + const dsa = { + dsarequired: '3', + pubrender: '0', + datatopub: '2', + transparency: 20 + } + const request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, { ortb2: {regs: { + ext: { + dsa: deepClone(dsa) + } + } + }})[0]; + const r = extractPayload(request); + + expect(r.regs).to.be.undefined; + }); + it('should not set dsa transparency when fields arent appropriately defined', function () { + const dsa = { + transparency: [{ + domain: 3, + dsaparams: [1] + }, + { + domain: 'domain.com', + dsaparams: 'params' + }, + { + domain: 'domain.com', + dsaparams: ['1'] + }] + } + const request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, { ortb2: {regs: { + ext: { + dsa: deepClone(dsa) + } + } + }})[0]; + const r = extractPayload(request); + + expect(r.regs).to.be.undefined; + }); it('should set gpp and gpp_sid field when defined', function () { const request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, { ortb2: {regs: {gpp: 'gpp', gpp_sid: [1]}} })[0]; const r = extractPayload(request); @@ -1792,7 +2016,7 @@ describe('IndexexchangeAdapter', function () { expect(r.regs.gpp_sid).to.be.an('array'); expect(r.regs.gpp_sid).to.include(1); }); - it('should not set gpp and gpp_sid field when not defined', function () { + it('should not set gpp, gpp_sid and dsa field when not defined', function () { const request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, { ortb2: {regs: {}} })[0]; const r = extractPayload(request); @@ -3410,6 +3634,40 @@ describe('IndexexchangeAdapter', function () { expect(result[0]).to.deep.equal(expectedParse[0]); }); + it('should get correct bid response for banner ad with dsa signals', function () { + const expectedParse = [ + { + requestId: '1a2b3c4d', + cpm: 1, + creativeId: '12345', + width: 300, + height: 250, + mediaType: 'banner', + ad: '', + currency: 'USD', + ttl: 300, + netRevenue: true, + meta: { + networkId: 50, + brandId: 303325, + brandName: 'OECTA', + advertiserDomains: ['www.abc.com'], + dsa: { + behalf: 'Advertiser', + paid: 'Advertiser', + transparency: [{ + domain: 'dsp1domain.com', + dsaparams: [1, 2] + }], + 'adrender': 1 + } + } + } + ]; + const result = spec.interpretResponse({ body: DEFAULT_BANNER_BID_RESPONSE_WITH_DSA }, bannerBidderRequest); + expect(result[0]).to.deep.equal(expectedParse[0]); + }); + it('should get correct bid response for banner ad with missing adomain', function () { const expectedParse = [ { diff --git a/test/spec/modules/jixieBidAdapter_spec.js b/test/spec/modules/jixieBidAdapter_spec.js index fa7618814f8..5428fd0db0f 100644 --- a/test/spec/modules/jixieBidAdapter_spec.js +++ b/test/spec/modules/jixieBidAdapter_spec.js @@ -707,4 +707,67 @@ describe('jixie Adapter', function () { expect(jixieaux.ajax.calledWith(TRACKINGURL_)).to.equal(true); }) }); // describe + + describe('getUserSyncs', function () { + it('it should favour iframe over pixel if publisher allows iframe usersync', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true, + } + const response = { + 'userSyncs': [ + { + 'uf': 'https://syncstuff.jixie.io/', + 'up': 'https://syncstuff.jixie.io/image.gif' + }, + { + 'up': 'https://syncstuff.jixie.io/image1.gif' + } + ] + } + let result = spec.getUserSyncs(syncOptions, [{ body: response }]); + expect(result[0].type).to.equal('iframe') + expect(result[1].type).to.equal('image') + }) + + it('it should pick pixel if publisher not allow iframe', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true, + } + const response = { + 'userSyncs': [ + { + 'uf': 'https://syncstuff.jixie.io/', + 'up': 'https://syncstuff.jixie.io/image.gif' + }, + { + 'up': 'https://syncstuff.jixie.io/image1.gif' + } + ] + } + let result = spec.getUserSyncs(syncOptions, [{ body: response }]); + expect(result[0].type).to.equal('image') + expect(result[1].type).to.equal('image') + }) + + it('it should return nothing if pub only allow pixel but all usersyncs are iframe only', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true, + } + const response = { + 'userSyncs': [ + { + 'uf': 'https://syncstuff.jixie.io/', + }, + { + 'uf': 'https://syncstuff2.jixie.io/', + } + ] + } + let result = spec.getUserSyncs(syncOptions, [{ body: response }]); + expect(result.length).to.equal(0) + }) + }) }); diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 4638595e0d6..12fe9a7e800 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -237,6 +237,41 @@ describe('jwplayerRtdProvider', function() { fetchTargetingForMediaId(mediaIdWithSegment); + const bid = {}; + const adUnit = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: mediaIdWithSegment, + playerDivId: validPlayerID + } + } + } + }, + bids: [ + bid + ] + }; + const expectedContentId = 'jw_' + mediaIdWithSegment; + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId + } + }; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); + server.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('includes backwards support for playerID when playerDivId is not set', function () { + const bidRequestSpy = sinon.spy(); + + fetchTargetingForMediaId(mediaIdWithSegment); + const bid = {}; const adUnit = { ortb2Imp: { @@ -605,23 +640,23 @@ describe('jwplayerRtdProvider', function() { it('should prioritize adUnit properties ', function () { const expectedMediaID = 'test_media_id'; const expectedPlayerID = 'test_player_id'; - const config = { playerID: 'bad_id', mediaID: 'bad_id' }; + const config = { playerDivId: 'bad_id', mediaID: 'bad_id' }; - const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID, playerID: expectedPlayerID } } } } }; + const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID, playerDivId: expectedPlayerID } } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.have.property('mediaID', expectedMediaID); - expect(targeting).to.have.property('playerID', expectedPlayerID); + expect(targeting).to.have.property('playerDivId', expectedPlayerID); }); it('should use config properties as fallbacks', function () { const expectedMediaID = 'test_media_id'; const expectedPlayerID = 'test_player_id'; - const config = { playerID: expectedPlayerID, mediaID: 'bad_id' }; + const config = { playerDivId: expectedPlayerID, mediaID: 'bad_id' }; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID } } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.have.property('mediaID', expectedMediaID); - expect(targeting).to.have.property('playerID', expectedPlayerID); + expect(targeting).to.have.property('playerDivId', expectedPlayerID); }); it('should return undefined when Publisher Params are absent', function () { diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index f43c3b11aac..20a43d0397f 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -479,6 +479,21 @@ describe('kargo adapter tests', function () { floor: 1, fpd: { gpid: '/22558409563,18834096/dfy_mobile_adhesion' + }, + ext: { + ortb2Imp: { + ext: { + tid: '10101', + data: { + adServer: { + name: 'gam', + adslot: '/22558409563,18834096/dfy_mobile_adhesion' + }, + pbadslot: '/22558409563,18834096/dfy_mobile_adhesion' + }, + gpid: '/22558409563,18834096/dfy_mobile_adhesion' + } + } } }, { @@ -491,6 +506,21 @@ describe('kargo adapter tests', function () { }, fpd: { gpid: '/22558409563,18834096/dfy_mobile_adhesion' + }, + floor: 2, + ext: { + ortb2Imp: { + ext: { + tid: '20202', + data: { + adServer: { + name: 'gam', + adslot: '/22558409563,18834096/dfy_mobile_adhesion' + }, + pbadslot: '/22558409563,18834096/dfy_mobile_adhesion' + } + } + } } }, { @@ -503,6 +533,21 @@ describe('kargo adapter tests', function () { }, fpd: { gpid: '/22558409563,18834096/dfy_mobile_adhesion' + }, + floor: 3, + ext: { + ortb2Imp: { + ext: { + tid: '30303', + data: { + adServer: { + name: 'gam', + adslot: '/22558409563,18834096/dfy_mobile_adhesion' + } + }, + gpid: '/22558409563,18834096/dfy_mobile_adhesion' + } + } } } ], @@ -553,6 +598,56 @@ describe('kargo adapter tests', function () { ] } ] + }, + ext: { + ortb2: { + device: { + sua: { + platform: { + brand: 'macOS', + version: ['12', '6', '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: 1, + model: 'model', + source: 1, + } + }, + site: { + id: '1234', + name: 'SiteName', + cat: ['IAB1', 'IAB2', 'IAB3'] + }, + user: { + data: [ + { + name: 'prebid.org', + ext: { + segtax: 600, + segclass: 'v1', + }, + segment: [ + { + id: '133' + }, + ] + }, + ] + } + } } }; @@ -605,6 +700,16 @@ describe('kargo adapter tests', function () { payload['gdprConsent'] = gdpr } + clonedBids.forEach(bid => { + if (bid.mediaTypes.banner) { + bid.getFloor = () => ({ currency: 'USD', floor: 1 }); + } else if (bid.mediaTypes.video) { + bid.getFloor = () => ({ currency: 'USD', floor: 2 }); + } else if (bid.mediaTypes.native) { + bid.getFloor = () => ({ currency: 'USD', floor: 3 }); + } + }); + var request = spec.buildRequests(clonedBids, payload); var krakenParams = request.data; @@ -725,7 +830,8 @@ describe('kargo adapter tests', function () { adm: '
', width: 320, height: 50, - metadata: {} + metadata: {}, + creativeID: 'bar' }, 2: { id: 'bar', @@ -736,14 +842,16 @@ describe('kargo adapter tests', function () { targetingCustom: 'dmpmptest1234', metadata: { landingPageDomain: ['https://foobar.com'] - } + }, + creativeID: 'foo' }, 3: { id: 'bar', cpm: 2.5, adm: '
', width: 300, - height: 250 + height: 250, + creativeID: 'foo' }, 4: { id: 'bar', @@ -753,6 +861,7 @@ describe('kargo adapter tests', function () { height: 250, mediaType: 'banner', metadata: {}, + creativeID: 'foo', currency: 'EUR' }, 5: { @@ -763,6 +872,7 @@ describe('kargo adapter tests', function () { height: 250, mediaType: 'video', metadata: {}, + creativeID: 'foo', currency: 'EUR' }, 6: { @@ -774,6 +884,7 @@ describe('kargo adapter tests', function () { height: 250, mediaType: 'video', metadata: {}, + creativeID: 'foo', currency: 'EUR' } } @@ -818,7 +929,7 @@ describe('kargo adapter tests', function () { width: 320, height: 50, ttl: 300, - creativeId: 'foo', + creativeId: 'bar', dealId: undefined, netRevenue: true, currency: 'USD', @@ -833,7 +944,7 @@ describe('kargo adapter tests', function () { width: 300, height: 250, ttl: 300, - creativeId: 'bar', + creativeId: 'foo', dealId: 'dmpmptest1234', netRevenue: true, currency: 'USD', @@ -850,7 +961,7 @@ describe('kargo adapter tests', function () { width: 300, height: 250, ttl: 300, - creativeId: 'bar', + creativeId: 'foo', dealId: undefined, netRevenue: true, currency: 'USD', @@ -865,7 +976,7 @@ describe('kargo adapter tests', function () { width: 300, height: 250, ttl: 300, - creativeId: 'bar', + creativeId: 'foo', dealId: undefined, netRevenue: true, currency: 'EUR', @@ -880,7 +991,7 @@ describe('kargo adapter tests', function () { height: 250, vastXml: '', ttl: 300, - creativeId: 'bar', + creativeId: 'foo', dealId: undefined, netRevenue: true, currency: 'EUR', @@ -895,7 +1006,7 @@ describe('kargo adapter tests', function () { height: 250, vastUrl: 'https://foobar.com/vast_adm', ttl: 300, - creativeId: 'bar', + creativeId: 'foo', dealId: undefined, netRevenue: true, currency: 'EUR', diff --git a/test/spec/modules/kimberliteBidAdapter_spec.js b/test/spec/modules/kimberliteBidAdapter_spec.js new file mode 100644 index 00000000000..1480f1cc768 --- /dev/null +++ b/test/spec/modules/kimberliteBidAdapter_spec.js @@ -0,0 +1,171 @@ +import { spec } from 'modules/kimberliteBidAdapter.js'; +import { assert } from 'chai'; +import { BANNER } from '../../../src/mediaTypes.js'; + +const BIDDER_CODE = 'kimberlite'; + +describe('kimberliteBidAdapter', function () { + const sizes = [[640, 480]]; + + describe('isBidRequestValid', function () { + let bidRequest; + + beforeEach(function () { + bidRequest = { + mediaTypes: { + [BANNER]: { + sizes: [[320, 240]] + } + }, + params: { + placementId: 'test-placement' + } + }; + }); + + it('pass on valid bidRequest', function () { + assert.isTrue(spec.isBidRequestValid(bidRequest)); + }); + + it('fails on missed placementId', function () { + delete bidRequest.params.placementId; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('fails on empty banner', function () { + delete bidRequest.mediaTypes.banner; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('fails on empty banner.sizes', function () { + delete bidRequest.mediaTypes.banner.sizes; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('fails on empty request', function () { + assert.isFalse(spec.isBidRequestValid()); + }); + }); + + describe('buildRequests', function () { + let bidRequests, bidderRequest; + + beforeEach(function () { + bidRequests = [{ + mediaTypes: { + [BANNER]: {sizes: sizes} + }, + params: { + placementId: 'test-placement' + } + }]; + + bidderRequest = { + refererInfo: { + domain: 'example.com', + page: 'https://www.example.com/test.html', + }, + bids: [{ + mediaTypes: { + [BANNER]: {sizes: sizes} + } + }] + }; + }); + + it('valid bid request', function () { + const bidRequest = spec.buildRequests(bidRequests, bidderRequest); + assert.equal(bidRequest.method, 'POST'); + assert.ok(bidRequest.data); + + const requestData = bidRequest.data; + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + expect(requestData.site.publisher.domain).to.equal(bidderRequest.refererInfo.domain); + + expect(requestData.imp).to.be.an('array').and.is.not.empty; + + expect(requestData.ext).to.be.an('Object').and.have.all.keys('prebid'); + expect(requestData.ext.prebid).to.be.an('Object').and.have.all.keys('ver', 'adapterVer'); + + const impData = requestData.imp[0]; + expect(impData.banner).is.to.be.an('Object').and.have.all.keys(['format', 'topframe']); + + const bannerData = impData.banner; + expect(bannerData.format).to.be.an('array').and.is.not.empty; + + const formatData = bannerData.format[0]; + expect(formatData).to.be.an('Object').and.have.all.keys('w', 'h'); + + assert.equal(formatData.w, sizes[0][0]); + assert.equal(formatData.h, sizes[0][1]); + }); + }); + + describe('interpretResponse', function () { + let bidderResponse, bidderRequest, bidRequest, expectedBid; + + const requestId = '07fba8b0-8812-4dc6-b91e-4a525d81729c'; + const bidId = '222209853178'; + const impId = 'imp-id'; + const crId = 'creative-id'; + const adm = 'landing'; + + beforeEach(function () { + bidderResponse = { + body: { + id: requestId, + seatbid: [{ + bid: [{ + crid: crId, + id: bidId, + impid: impId, + price: 1, + adm: adm + }] + }] + } + }; + + bidderRequest = { + refererInfo: { + domain: 'example.com', + page: 'https://www.example.com/test.html', + }, + bids: [{ + bidId: impId, + mediaTypes: { + [BANNER]: {sizes: sizes} + }, + params: { + placementId: 'test-placement' + } + }] + }; + + expectedBid = { + mediaType: 'banner', + requestId: 'imp-id', + seatBidId: '222209853178', + cpm: 1, + creative_id: 'creative-id', + creativeId: 'creative-id', + ttl: 300, + netRevenue: true, + ad: adm, + meta: {} + }; + + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + }); + + it('pass on valid request', function () { + const bids = spec.interpretResponse(bidderResponse, bidRequest); + assert.deepEqual(bids[0], expectedBid); + }); + + it('fails on empty response', function () { + const bids = spec.interpretResponse({body: ''}, bidRequest); + assert.empty(bids); + }); + }); +}); 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 ad21d7b6763..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(); }); @@ -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,41 +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 sovrn 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 seperate object when present', function() { + 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 seperate object when present', function() { + 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 seperate object when present', function () { + 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 seperate object when present', function() { + 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 3af598c5d4e..c6108b49715 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -2,6 +2,8 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } f import * as utils from 'src/utils.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'; @@ -16,6 +18,7 @@ describe('LiveIntentId', function() { let getCookieStub; let getDataFromLocalStorageStub; let imgStub; + let refererInfoStub; beforeEach(function() { liveIntentIdSubmodule.setModuleMode('standard'); @@ -26,6 +29,7 @@ describe('LiveIntentId', function() { uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + refererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); }); afterEach(function() { @@ -36,6 +40,7 @@ describe('LiveIntentId', function() { uspConsentDataStub.restore(); gdprConsentDataStub.restore(); gppConsentDataStub.restore(); + refererInfoStub.restore(); resetLiveIntentIdSubmodule(); }); @@ -423,6 +428,13 @@ describe('LiveIntentId', function() { 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/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/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/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 0864a976d7d..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'; @@ -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 @@ -1707,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/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index 601bcff527a..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 = { @@ -161,6 +173,47 @@ 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 = ''; @@ -207,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/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index d7984c05967..cdeae38aa19 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -102,10 +102,34 @@ describe('MediaSquare bid adapter tests', function () { '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', @@ -144,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 () { @@ -176,6 +202,7 @@ describe('MediaSquare bid adapter tests', function () { 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); diff --git a/test/spec/modules/mgidXBidAdapter_spec.js b/test/spec/modules/mgidXBidAdapter_spec.js index 978ca3de036..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', } } @@ -108,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 () { @@ -193,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 7e22114aeed..d5d6cdc5449 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -301,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/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index f84981352ab..ab1fbdcc074 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -137,6 +137,10 @@ describe('Missena Adapter', 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( diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index f57db82aedc..ff58671b17b 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -19,12 +19,22 @@ describe('nextMillenniumBidAdapterTests', () => { mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, adUnitCode: 'test-banner-1', }, + + mediaTypes: { + banner: { + data: {sizes: [[300, 250], [320, 250]]}, + bidfloorcur: 'EUR', + bidfloor: 1.11, + }, + }, }, expected: { id: 'test-banner-1', + bidfloorcur: 'EUR', + bidfloor: 1.11, ext: {prebid: {storedrequest: {id: '123'}}}, - banner: {format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + banner: {w: 300, h: 250, format: [{w: 300, h: 250}, {w: 320, h: 250}]}, }, }, @@ -33,23 +43,63 @@ describe('nextMillenniumBidAdapterTests', () => { data: { id: '234', bid: { - mediaTypes: {video: {playerSize: [400, 300]}}, + 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', + }, + }, }, 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: 400, h: 300}, + 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} = data; - const imp = getImp(bid, id); + const {bid, id, mediaTypes} = data; + const imp = getImp(bid, id, mediaTypes); expect(imp).to.deep.equal(expected); }); } @@ -373,6 +423,28 @@ describe('nextMillenniumBidAdapterTests', () => { }}, }, + { + 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: { @@ -862,7 +934,7 @@ describe('nextMillenniumBidAdapterTests', () => { ]; 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 7091bb56631..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('3.0'); + expect(requestContent.ext.bidderVersion).to.be.eql('4.0'); expect(requestContent.ext.source).to.be.eql('prebid.js'); }); @@ -561,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); @@ -585,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 ' } } ], @@ -602,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 index 06a39ffd020..f6c741bb7ff 100644 --- a/test/spec/modules/nobidAnalyticsAdapter_spec.js +++ b/test/spec/modules/nobidAnalyticsAdapter_spec.js @@ -127,9 +127,9 @@ describe('NoBid Prebid Analytic', function () { mediaType: 'banner', source: 'client', cpm: 6.4, + currency: 'EUR', creativeId: 'TEST', dealId: '', - currency: 'USD', netRevenue: true, ttl: 300, ad: 'AD HERE', @@ -167,13 +167,17 @@ describe('NoBid Prebid Analytic', function () { ] }; - const requestOutgoing = { + 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', @@ -197,16 +201,20 @@ describe('NoBid Prebid Analytic', function () { clock.tick(5000); expect(server.requests).to.have.length(1); const bidWonRequest = JSON.parse(server.requests[0].requestBody); - expect(bidWonRequest).to.have.property('bidderCode', requestOutgoing.bidderCode); - expect(bidWonRequest).to.have.property('statusMessage', requestOutgoing.statusMessage); - expect(bidWonRequest).to.have.property('adId', requestOutgoing.adId); - expect(bidWonRequest).to.have.property('requestId', requestOutgoing.requestId); - expect(bidWonRequest).to.have.property('mediaType', requestOutgoing.mediaType); - expect(bidWonRequest).to.have.property('cpm', requestOutgoing.cpm); - expect(bidWonRequest).to.have.property('adUnitCode', requestOutgoing.adUnitCode); - expect(bidWonRequest).to.have.property('timeToRespond', requestOutgoing.timeToRespond); - expect(bidWonRequest).to.have.property('size', requestOutgoing.size); - expect(bidWonRequest).to.have.property('topLocation', requestOutgoing.topLocation); + 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(); @@ -304,10 +312,10 @@ describe('NoBid Prebid Analytic', function () { auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', mediaType: 'banner', source: 'client', - cpm: 6.44, + cpm: 5.93, + currency: 'EUR', creativeId: 'TEST', dealId: '', - currency: 'USD', netRevenue: true, ttl: 300, ad: '', @@ -336,7 +344,7 @@ describe('NoBid Prebid Analytic', function () { timeout: 3000 }; - const requestOutgoing = { + const expectedOutgoingRequest = { auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', bidderRequests: [ { @@ -344,7 +352,6 @@ describe('NoBid Prebid Analytic', function () { bidderRequestId: '7c1940bb285731', bids: [ { - bidder: 'nobid', params: { siteId: SITE_ID }, mediaTypes: { banner: { sizes: [[728, 90]] } }, adUnitCode: 'leaderboard', @@ -365,7 +372,10 @@ describe('NoBid Prebid Analytic', function () { width: 728, height: 90, mediaType: 'banner', - cpm: 6.44, + cpm: 5.93, + currency: 'EUR', + originalCpm: 6.44, + originalCurrency: 'USD', adUnitCode: 'leaderboard' } ] @@ -388,21 +398,34 @@ describe('NoBid Prebid Analytic', function () { clock.tick(5000); expect(server.requests).to.have.length(1); const auctionEndRequest = JSON.parse(server.requests[0].requestBody); - expect(auctionEndRequest).to.have.property('auctionId', requestOutgoing.auctionId); + 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]).to.have.property('bidderCode', requestOutgoing.bidderRequests[0].bidderCode); + expect(auctionEndRequest.bidderRequests[0].bidderCode).to.equal(expectedOutgoingRequest.bidderRequests[0].bidderCode); expect(auctionEndRequest.bidderRequests[0].bids).to.have.length(1); - expect(auctionEndRequest.bidderRequests[0].bids[0]).to.have.property('bidder', requestOutgoing.bidderRequests[0].bids[0].bidder); - expect(auctionEndRequest.bidderRequests[0].bids[0]).to.have.property('adUnitCode', requestOutgoing.bidderRequests[0].bids[0].adUnitCode); - expect(auctionEndRequest.bidderRequests[0].bids[0].params).to.have.property('siteId', requestOutgoing.bidderRequests[0].bids[0].params.siteId); - expect(auctionEndRequest.bidderRequests[0].refererInfo).to.have.property('topmostLocation', requestOutgoing.bidderRequests[0].refererInfo.topmostLocation); + 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: false})); + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 0})); disabled = nobidAnalytics.isAnalyticsDisabled(); expect(disabled).to.equal(false); events.emit(constants.EVENTS.AUCTION_END, {auctionId: '1234567890'}); @@ -417,7 +440,7 @@ describe('NoBid Prebid Analytic', function () { clock.tick(1000); expect(server.requests).to.have.length(3); - nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 1})); disabled = nobidAnalytics.isAnalyticsDisabled(); expect(disabled).to.equal(true); events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); @@ -425,7 +448,7 @@ describe('NoBid Prebid Analytic', function () { expect(server.requests).to.have.length(3); nobidAnalytics.retentionSeconds = 5; - nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + nobidAnalytics.processServerResponse(JSON.stringify({disabled: 1})); clock.tick(1000); disabled = nobidAnalytics.isAnalyticsDisabled(); expect(disabled).to.equal(true); @@ -437,6 +460,100 @@ describe('NoBid Prebid Analytic', function () { }); }); + 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([]); @@ -456,7 +573,8 @@ describe('NoBid Prebid Analytic', function () { let active = nobidCarbonizer.isActive(); expect(active).to.equal(false); - active = nobidCarbonizer.isActive(JSON.stringify({carbonizer_active: false})); + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: false})); + active = nobidCarbonizer.isActive(); expect(active).to.equal(false); nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); @@ -469,12 +587,12 @@ describe('NoBid Prebid Analytic', function () { let stored = nobidCarbonizer.getStoredLocalData(); expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain(`{"carbonizer_active":true,"ts":`); clock.tick(5000); - active = nobidCarbonizer.isActive(adunits, true); + active = nobidCarbonizer.isActive(); expect(active).to.equal(false); nobidAnalytics.retentionSeconds = previousRetention; nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); - active = nobidCarbonizer.isActive(adunits, true); + active = nobidCarbonizer.isActive(); expect(active).to.equal(true); let adunits = [ 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 7960574c1a4..93db5ffc57f 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -273,6 +273,7 @@ describe('onetag', function () { }); 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', @@ -280,7 +281,8 @@ describe('onetag', function () { 'timeout': 3000, 'gdprConsent': { consentString: consentString, - gdprApplies: true + gdprApplies: true, + addtlConsent: addtlConsent } }; let serverRequest = spec.buildRequests([bannerBid], bidderRequest); @@ -289,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 () { @@ -396,47 +399,75 @@ describe('onetag', function () { expect(payload.ortb2).to.exist; expect(payload.ortb2).to.exist.and.to.deep.equal(firtPartyData); }); - }); - 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); + 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 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(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); + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(false); + }); }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); @@ -486,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/'; 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 1af0fce103d..7c504bca50b 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1508,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/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/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 index 6d8f9a66bcf..f2504a810c4 100644 --- a/test/spec/modules/pangleBidAdapter_spec.js +++ b/test/spec/modules/pangleBidAdapter_spec.js @@ -127,11 +127,15 @@ describe('pangle bid adapter', function () { describe('buildRequests', function () { it('creates request data', function () { - let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; - expect(request).to.exist.and.to.be.a('object'); - const payload = request.data; - expect(payload.imp[0]).to.have.property('id', REQUEST[0].bidId); - expect(payload.imp[1]).to.have.property('id', REQUEST[1].bidId); + 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); }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 5b591804b95..2bab144dae7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -91,6 +91,7 @@ const REQUEST = { } }, 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', + 'adUnitId': 'au-id-1', 'bids': [ { 'bid_id': '123', @@ -2882,6 +2883,32 @@ describe('S2S Adapter', function () { }) }) + 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 }); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index a31f07fecef..7ea7722b12a 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -116,7 +116,8 @@ 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') { @@ -618,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' @@ -632,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' @@ -730,6 +731,23 @@ describe('the price floors module', function () { 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 () { @@ -2345,7 +2363,7 @@ describe('the price floors module', function () { } const resp = { - transactionId: req.transactionId, + adUnitId: req.adUnitId, size: [100, 100], mediaType: 'banner', } @@ -2356,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/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/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 c6447905ecd..951b5135260 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -100,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' }, @@ -606,6 +606,7 @@ 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); @@ -640,6 +641,7 @@ 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.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; @@ -812,6 +814,7 @@ 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[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'); @@ -1035,6 +1038,7 @@ 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.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() { @@ -1149,6 +1153,7 @@ 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.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; @@ -1209,6 +1214,7 @@ describe('pubmatic analytics adapter', function () { 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(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'); @@ -1267,6 +1273,7 @@ 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.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'); @@ -1388,6 +1395,7 @@ 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.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() { @@ -1466,6 +1474,7 @@ 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'); @@ -1501,6 +1510,7 @@ 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.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; @@ -1596,6 +1606,7 @@ 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'); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 5d59ff99a89..fda2c853e87 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1781,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 = [ { @@ -3753,6 +3784,16 @@ describe('PubMatic adapter', function () { 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' @@ -3764,6 +3805,7 @@ describe('PubMatic adapter', function () { 'deal_channel': 1, 'bidtype': 0, advertiserId: 'adid', + dsa, // networkName: 'nwnm', // primaryCatId: 'pcid', // advertiserName: 'adnm', @@ -3795,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/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index 719e15ad695..f172d192221 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/rasBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import {getAdUnitSizes} from '../../../src/utils'; const CSR_ENDPOINT = 'https://csr.onet.pl/4178463/csr-006/csr.json?nid=4178463&'; @@ -64,6 +63,20 @@ describe('rasBidAdapter', function () { customParams: { test: 'name=value' } + }, + mediaTypes: { + banner: { + sizes: [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } } }; const bid2 = { @@ -75,6 +88,16 @@ describe('rasBidAdapter', function () { area: 'areatest', site: 'test', network: '4178463' + }, + mediaTypes: { + banner: { + sizes: [ + [ + 750, + 300 + ] + ] + } } }; @@ -128,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'); @@ -148,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 () { @@ -172,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); }); @@ -245,4 +302,309 @@ describe('rasBidAdapter', function () { 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/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 20c60ca328a..d2b173f53df 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -293,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') @@ -916,7 +916,7 @@ describe('Richaudience adapter tests', function () { it('onTimeout exist as a function', () => { expect(spec.onTimeout).to.exist.and.to.be.a('function'); }); - it('should send timeout', 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'); @@ -924,6 +924,13 @@ describe('Richaudience adapter tests', function () { }); 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'}}} @@ -1261,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 28bd123cb5d..ec9309fd4ae 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -324,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 f0e33ce940e..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 @@ -1711,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: { @@ -2215,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); @@ -2718,6 +2760,56 @@ describe('the rubicon adapter', function () { 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 () { @@ -2984,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); @@ -3001,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); @@ -3276,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', @@ -3733,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'); @@ -3767,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' }}); @@ -3824,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'); @@ -3839,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'); }); @@ -3893,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: { @@ -3962,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/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 6a63ae681e7..ab099d87429 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -425,6 +425,41 @@ describe('sharethrough adapter spec', function () { }); }); + 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', () => { it('should include transaction id when provided', () => { const requests = spec.buildRequests(bidRequests, bidderRequest); @@ -585,6 +620,27 @@ 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); + }); }); }); @@ -692,6 +748,22 @@ describe('sharethrough adapter spec', function () { 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; + }); + }); }); describe('interpretResponse', function () { 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 58b4cd8c0d0..b01d95e2a4c 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -787,7 +787,7 @@ describe('Smart bid adapter tests', function () { const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); - expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); + expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(1); }); it('Verify videoData params override meta values', function () { @@ -1097,7 +1097,7 @@ describe('Smart bid adapter tests', function () { const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); - expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); + expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(1); }); it('Verify videoData params override meta values', function () { @@ -1143,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 () { diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index 458ccc37759..1b592e142c3 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -61,7 +61,7 @@ describe('SmartyadsAdapter', function () { 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'); 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 3ba84228872..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', @@ -344,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/sovrnAnalyticsAdapter_spec.js b/test/spec/modules/sovrnAnalyticsAdapter_spec.js index 973e90abd5a..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 f165a6da6d1..274192d14a7 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -530,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/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index 71619424e4b..2f5fe104eb1 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -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'); }); 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/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index dd91c410d08..ca09fbbbcc9 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -6,6 +6,10 @@ 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(); @@ -153,7 +157,7 @@ describe('Taboola Adapter', function () { 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); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal({error, bidderRequest}); }); }); @@ -214,7 +218,11 @@ describe('Taboola Adapter', function () { 'ext': {}, }, 'regs': {'coppa': 0, 'ext': {}}, - 'ext': {} + 'ext': { + 'prebid': { + 'version': '$prebid.version$' + } + } }; expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); @@ -390,6 +398,30 @@ describe('Taboola Adapter', function () { 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); + }); }); describe('handle privacy segments when building request', function () { @@ -471,6 +503,90 @@ describe('Taboola Adapter', function () { 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); @@ -576,6 +692,181 @@ describe('Taboola Adapter', function () { } }; + 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, []) expect(res).to.be.an('array').that.is.empty @@ -732,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; @@ -892,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/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 98f706f5193..1e044651315 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/teadsBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import { off } from '../../../src/events'; +import * as autoplay from 'libraries/autoplayDetection/autoplay.js' const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; @@ -1005,6 +1005,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() { @@ -1020,7 +1059,8 @@ describe('teadsBidAdapter', () => { 'ttl': 360, 'width': 300, 'creativeId': 'er2ee', - 'placementId': 34 + 'placementId': 34, + 'needAutoplay': true }, { 'ad': AD_SCRIPT, 'cpm': 0.5, @@ -1031,7 +1071,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 + } + } }] } }; @@ -1057,7 +1109,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, @@ -1073,6 +1134,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/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 bc7df85db0d..4a79e7f77fd 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -384,6 +384,22 @@ describe('topics', () => { }); 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({ @@ -398,6 +414,10 @@ describe('topics', () => { }); }); + after(() => { + config.resetConfig(); + }) + it('should store segments if receiveMessage event is triggered with segment data', () => { receiveMessage(evt); let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); @@ -421,11 +441,13 @@ describe('handles fetch request for topics api headers', () => { beforeEach(() => { stubbedFetch = sinon.stub(window, 'fetch'); + reset(); }); afterEach(() => { stubbedFetch.restore(); storage.removeDataFromLocalStorage(topicStorageName); + config.resetConfig(); }); it('should make a fetch call when a fetchUrl is present for a selected bidder', () => { @@ -444,6 +466,7 @@ describe('handles fetch request for topics api headers', () => { }); stubbedFetch.returns(Promise.resolve(true)); + loadTopicsForBidders({ browsingTopics: true, featurePolicy: { diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 275b9b3bfee..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' } } ] diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 18f49f4943e..1e909d79ed4 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -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'; @@ -99,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, @@ -644,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: { @@ -679,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); @@ -3554,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/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/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 c5f088a2306..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 { NATIVE_ASSETS, spec } from 'modules/yandexBidAdapter.js'; import * as utils from 'src/utils.js'; -import { BANNER, NATIVE } from '../../../src/mediaTypes'; 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, @@ -107,12 +141,10 @@ describe('Yandex adapter', function () { }); it('should send eids and ortb2 user data if defined', function() { - const bidRequestExtra = { - userIdAsEids: [{ - source: 'sharedid.org', - uids: [{ id: '01', atype: 1 }], - }], + const bidderRequestWithUserData = { + ...bidderRequest, ortb2: { + ...bidderRequest.ortb2, user: { data: [ { @@ -127,17 +159,24 @@ describe('Yandex adapter', function () { }, ], }, - }, + } + }; + const bidRequestExtra = { + userIdAsEids: [{ + source: 'sharedid.org', + uids: [{ id: '01', atype: 1 }], + }], }; + const expected = { ext: { eids: bidRequestExtra.userIdAsEids, }, - data: bidRequestExtra.ortb2.user.data, + data: bidderRequestWithUserData.ortb2.user.data, }; const bannerRequest = getBidRequest(bidRequestExtra); - const requests = spec.buildRequests([bannerRequest], bidderRequest); + const requests = spec.buildRequests([bannerRequest], bidderRequestWithUserData); expect(requests).to.have.lengthOf(1); const request = requests[0]; @@ -149,6 +188,16 @@ describe('Yandex adapter', function () { 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({ @@ -496,41 +545,46 @@ describe('Yandex adapter', function () { }); it('Should not trigger pixel if bid does not contain nurl', function() { - const result = spec.onBidWon({}); + spec.onBidWon({}); + expect(utils.triggerPixel.callCount).to.equal(0) }) it('Should trigger pixel if bid has nurl', function() { - const result = spec.onBidWon({ + 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() { - const result = spec.onBidWon({ + 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() { - const result = spec.onBidWon({ + 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() { - const result = spec.onBidWon({ + 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') }) 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/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index edb3ef3af27..3c842c3a308 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -394,6 +394,42 @@ 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 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: [{ @@ -476,20 +512,6 @@ describe('YieldmoAdapter', function () { expect(buildVideoBidAndGetVideoParam().mimes).to.deep.equal(['video/mkv']); }); - it('should validate protocol in video bid request', function () { - expect( - spec.isBidRequestValid( - mockVideoBid({}, {}, { protocols: [2, 3, 11] }) - ) - ).to.be.true; - - expect( - spec.isBidRequestValid( - mockVideoBid({}, {}, { protocols: [2, 3, 10] }) - ) - ).to.be.false; - }); - describe('video.skip state check', () => { it('should not set video.skip if neither *.video.skip nor *.video.skippable is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['skippable'] = false; @@ -620,6 +642,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 5194a6a526a..54b61f19506 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -9,7 +9,7 @@ let events = require('src/events'); const EVENTS = { AUCTION_END: { - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'timestamp': 1638441234544, 'auctionEnd': 1638441234784, 'auctionStatus': 'completed', @@ -61,7 +61,7 @@ const EVENTS = { 600 ] ], - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83' + 'transactionId': '6b29369c' } ], 'adUnitCodes': [ @@ -70,7 +70,7 @@ const EVENTS = { 'bidderRequests': [ { 'bidderCode': 'zeta_global_ssp', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'bidderRequestId': '1207cb49191887', 'bids': [ { @@ -90,7 +90,7 @@ const EVENTS = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -103,7 +103,7 @@ const EVENTS = { ], 'bidId': '206be9a13236af', 'bidderRequestId': '1207cb49191887', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -126,7 +126,7 @@ const EVENTS = { }, { 'bidderCode': 'appnexus', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'bidderRequestId': '32b97f0a935422', 'bids': [ { @@ -149,7 +149,7 @@ const EVENTS = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -162,7 +162,7 @@ const EVENTS = { ], 'bidId': '41badc0e164c758', 'bidderRequestId': '32b97f0a935422', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -205,7 +205,7 @@ const EVENTS = { } }, 'adUnitCode': '/19968336/header-bid-tag-0', - 'transactionId': '6b29369c-0c2e-414e-be1f-5867aec18d83', + 'transactionId': '6b29369c', 'sizes': [ [ 300, @@ -218,7 +218,7 @@ const EVENTS = { ], 'bidId': '41badc0e164c758', 'bidderRequestId': '32b97f0a935422', - 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', + 'auctionId': '75e394d9', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, @@ -243,12 +243,12 @@ const EVENTS = { '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', @@ -268,7 +268,7 @@ const EVENTS = { 'hb_size': '480x320', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'viaplay.fi' + 'hb_adomain': 'example.adomain' } } ], @@ -311,12 +311,12 @@ const EVENTS = { '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', @@ -336,7 +336,7 @@ const EVENTS = { 'hb_size': '480x320', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'viaplay.fi' + 'hb_adomain': 'example.adomain' }, 'status': 'rendered', 'params': [ @@ -409,16 +409,19 @@ describe('Zeta Global SSP Analytics Adapter', function() { const auctionEnd = JSON.parse(requests[0].requestBody); const auctionSucceeded = JSON.parse(requests[1].requestBody); - expect(auctionEnd.adUnitCodes[0]).to.be.equal('/19968336/header-bid-tag-0'); + expect(auctionEnd.adUnitCodes).to.be.undefined; expect(auctionEnd.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); - expect(auctionEnd.auctionEnd).to.be.equal(1638441234784); - expect(auctionEnd.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + 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.noBids[0].bidder).to.be.equal('appnexus'); + 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-ccce-4978-9238-91e6a1ac88a1'); + 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'); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 9ef97019a98..f9cfe2dde6a 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -49,11 +49,17 @@ describe('Zeta Ssp Bid Adapter', function () { }, tags: { someTag: 444, + emptyTag: {}, + nullTag: null, + complexEmptyTag: { + empty: {}, + nullValue: null + } }, sid: 'publisherId', tagid: 'test_tag_id', site: { - page: 'testPage' + page: 'http://www.zetaglobal.com/page?param=value' }, app: { bundle: 'testBundle' @@ -126,6 +132,16 @@ describe('Zeta Ssp Bid Adapter', function () { userIdAsEids: eids, timeout: 500, ortb2: { + device: { + sua: { + mobile: 1, + architecture: 'arm', + platform: { + brand: 'Chrome', + version: ['102'] + } + } + }, user: { data: [ { @@ -193,6 +209,7 @@ describe('Zeta Ssp Bid Adapter', function () { id: '12345', seatbid: [ { + seat: '1', bid: [ { id: 'auctionId', @@ -218,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: { @@ -246,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: { @@ -295,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'); }); @@ -585,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 () { @@ -594,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 () { @@ -608,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 () { @@ -622,6 +642,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 provide segments into the request', function () { @@ -632,4 +653,30 @@ describe('Zeta Ssp Bid Adapter', function () { 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 9cfee6f5cd8..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 () { @@ -646,6 +588,58 @@ describe('native.js', function () { 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'); + }); + }); }); describe('validate native openRTB', function () { @@ -724,7 +718,7 @@ describe('validate native openRTB', function () { describe('validate native', function () { const adUnit = { - transactionId: 'test_adunit', + adUnitId: 'test_adunit', mediaTypes: { native: { title: { @@ -749,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: { @@ -776,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: { @@ -799,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: { @@ -836,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: { @@ -1043,7 +1037,7 @@ describe('validate native', function () { const validBidRequests = [{ bidId: 'bidId3', adUnitCode: 'adUnitCode3', - transactionId: 'transactionId3', + adUnitId: 'transactionId3', mediaTypes: { banner: {} }, 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 98d841d9c7c..dac70696b4b 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -964,25 +964,42 @@ describe('adapterManager tests', function () { 'start': 1462918897460 }]; - it('invokes callBids on the S2S adapter', function () { - const done = sinon.stub(); - const onTimelyResponse = sinon.stub(); - prebidServerAdapterMock.callBids.callsFake((_1, _2, _3, done) => { - done(); + 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)); }); - adapterManager.callBids( - getAdUnits(), - bidRequests, - () => {}, - done, - undefined, - undefined, - onTimelyResponse - ); - sinon.assert.calledTwice(prebidServerAdapterMock.callBids); - sinon.assert.calledTwice(done); - 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 index a3a0459b980..dd03ad1a761 100644 --- a/test/spec/unit/core/ajax_spec.js +++ b/test/spec/unit/core/ajax_spec.js @@ -232,7 +232,7 @@ describe('attachCallbacks', () => { }; } - function expectNullXHR(response) { + function expectNullXHR(response, reason) { return new Promise((resolve, reject) => { attachCallbacks(Promise.resolve(response), { success: () => { @@ -246,7 +246,8 @@ describe('attachCallbacks', () => { statusText: '', responseText: '', response: '', - responseXML: null + responseXML: null, + reason }); expect(xhr.getResponseHeader('any')).to.be.null; resolve(); @@ -256,9 +257,21 @@ describe('attachCallbacks', () => { } it('runs error callback on rejections', () => { - return expectNullXHR(Promise.reject(new Error())); + 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, @@ -368,8 +381,9 @@ describe('attachCallbacks', () => { }); it(`runs error callback if body cannot be retrieved`, () => { - response.text = () => Promise.reject(new Error()); - return expectNullXHR(response); + const err = new Error(); + response.text = () => Promise.reject(err); + return expectNullXHR(response, err); }); if (success) { 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 9dcdb627698..5fe5a1accfc 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -38,10 +38,6 @@ const MOCK_BIDS_REQUEST = { ] } -function onTimelyResponseStub() { - -} - before(() => { hook.ready(); }); @@ -49,6 +45,10 @@ before(() => { let wrappedCallback = config.callbackWithBidder(CODE); describe('bidderFactory', () => { + let onTimelyResponseStub; + beforeEach(() => { + onTimelyResponseStub = sinon.stub(); + }) describe('bidders created by newBidder', function () { let spec; let bidder; @@ -582,6 +582,14 @@ describe('bidderFactory', () => { utils.logError.restore(); }); + 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); + }) + it('should call spec.interpretResponse() with the response content', function () { const bidder = newBidder(spec); @@ -800,12 +808,13 @@ describe('bidderFactory', () => { let ajaxStub; let callBidderErrorStub; let eventEmitterStub; - let xhrErrorMock = { - status: 500, - statusText: 'Internal Server Error' - }; + let xhrErrorMock; beforeEach(function () { + xhrErrorMock = { + status: 500, + statusText: 'Internal Server Error' + }; ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { callbacks.error('ajax call failed.', xhrErrorMock); }); @@ -821,6 +830,20 @@ describe('bidderFactory', () => { eventEmitterStub.restore(); }); + 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); + }) + }) + it('should not spec.interpretResponse()', function () { const bidder = newBidder(spec); @@ -1086,7 +1109,7 @@ describe('bidderFactory', () => { if (FEATURES.NATIVE) { it('should add native bids that do have required assets', function () { adUnits = [{ - transactionId: 'au', + adUnitId: 'au', nativeParams: { title: {'required': true}, } @@ -1097,7 +1120,7 @@ describe('bidderFactory', () => { bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', - transactionId: 'au', + adUnitId: 'au', params: { param: 5 }, @@ -1433,67 +1456,95 @@ describe('bidderFactory', () => { transactionId: 'au', }] }; - const fledgeAuctionConfig = { + const paapiConfig = { bidId: '1', config: { foo: 'bar' } } - describe('when response has FLEDGE auction config', function() { - let fledgeStub; - function fledgeHook(next, ...args) { - fledgeStub(...args); + it('should unwrap bids', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(addBidResponseStub, 'mock/placement', sinon.match(bids[0])); + }); + + it('does not unwrap bids from a bid that happens to have a "bids" property', () => { + const bidder = newBidder(spec); + const bid = Object.assign({ + bids: ['a', 'b'] + }, bids[0]); + spec.interpretResponse.returns(bid); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(addBidResponseStub, 'mock/placement', sinon.match(bid)); + }) + + describe('when response has PAAPI auction config', function() { + let paapiStub; + + function paapiHook(next, ...args) { + paapiStub(...args); } before(() => { - addComponentAuction.before(fledgeHook); + addComponentAuction.before(paapiHook); }); after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); + addComponentAuction.getHooks({hook: paapiHook}).remove(); }) beforeEach(function () { - fledgeStub = sinon.stub(); + paapiStub = 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'); - }); + const PAAPI_PROPS = ['fledgeAuctionConfigs', 'paapiAuctionConfigs']; - it('should call fledgeManager with FLEDGE configs', function() { + it(`should not accept both ${PAAPI_PROPS.join(' and ')}`, () => { const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [fledgeAuctionConfig] - }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(fledgeStub.calledOnce).to.equal(true); - sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + spec.interpretResponse.returns(Object.fromEntries(PAAPI_PROPS.map(prop => [prop, [paapiConfig]]))) + expect(() => { + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + }).to.throw; }) - it('should call fledgeManager with FLEDGE configs even if no bids returned', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: [], - fledgeAuctionConfigs: [fledgeAuctionConfig] - }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + 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'); + }) - expect(fledgeStub.calledOnce).to.be.true; - sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(false); + 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); + }) + }) + }) }) }) }) diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 39123d4aa41..7f55a2cddf0 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -26,6 +26,8 @@ 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; @@ -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], - }; - - resizeRemoteCreative(mockAdObject); - - 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,10 +1211,14 @@ describe('Unit: Prebid Module', function () { height: 0 } }, + body: { + appendChild: sinon.stub() + }, getElementsByTagName: sinon.stub(), querySelector: sinon.stub(), createElement: sinon.stub(), }; + doc.defaultView.document = doc; elStub = { insertBefore: sinon.stub() @@ -1532,6 +1515,7 @@ describe('Unit: Prebid Module', function () { } }, transactionId: 'mock-tid', + adUnitId: 'mock-au', bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, ] @@ -1606,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, @@ -1684,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, @@ -1970,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(() => { @@ -3546,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' } ] @@ -3555,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 895bf03165a..a7be4e327f0 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,4 +1,4 @@ -import {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'; @@ -7,14 +7,25 @@ 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 * as creativeRenderers from 'src/creativeRenderers.js'; import 'src/prebid.js'; +import 'modules/nativeRendering.js'; import {expect} from 'chai'; -import {handleRender} from '../../../src/adRendering.js'; 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) } @@ -53,48 +64,6 @@ describe('secureCreatives', () => { }); }); - describe('handleRender', () => { - let bidResponse, renderFn, result; - beforeEach(() => { - result = null; - renderFn = sinon.stub().callsFake((r) => { result = r; }); - bidResponse = { - adId: 123 - } - }); - - it('does not invoke renderFn, but the renderer instead, if the ad has one', () => { - const renderer = { - url: 'some-custom-renderer', - render: sinon.spy() - } - handleRender(renderFn, {bidResponse: {renderer}}); - sinon.assert.notCalled(renderFn); - sinon.assert.called(renderer.render); - }); - - ['ad', 'adUrl'].forEach((prop) => { - describe(`on ${prop}`, () => { - it('replaces AUCTION_PRICE macro', () => { - bidResponse[prop] = 'pre${AUCTION_PRICE}post'; - bidResponse.cpm = 123; - handleRender(renderFn, {adId: 123, bidResponse}); - expect(result[prop]).to.eql('pre123post'); - }); - it('replaces CLICKTHROUGH macro', () => { - bidResponse[prop] = 'pre${CLICKTHROUGH}post'; - handleRender(renderFn, {adId: 123, bidResponse, options: {clickUrl: 'clk'}}); - expect(result[prop]).to.eql('preclkpost'); - }); - it('defaults CLICKTHROUGH to empty string', () => { - bidResponse[prop] = 'pre${CLICKTHROUGH}post'; - handleRender(renderFn, {adId: 123, bidResponse}); - expect(result[prop]).to.eql('prepost'); - }); - }); - }); - }); - describe('receiveMessage', function() { const bidId = 1; const warning = `Ad id ${bidId} has been rendered before`; @@ -156,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; }); @@ -312,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() { @@ -372,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', () => { @@ -464,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/ttlCollection_spec.js b/test/spec/unit/utils/ttlCollection_spec.js index 29c6c438855..76cfa32d955 100644 --- a/test/spec/unit/utils/ttlCollection_spec.js +++ b/test/spec/unit/utils/ttlCollection_spec.js @@ -67,6 +67,33 @@ describe('ttlCollection', () => { }); }); + 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}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 098582c0af6..c84fe124db6 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1002,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() { diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index c746fdd2afd..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); diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 35d0a4fef24..3252c58c687 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -82,10 +82,10 @@ describe('video.js', function () { const bid = { adId: '456xyz', vastUrl: 'http://www.example.com/vastUrl', - transactionId: 'au' + adUnitId: 'au' }; const adUnits = [{ - transactionId: 'au', + adUnitId: 'au', mediaTypes: { video: {context: 'instream'} } @@ -96,10 +96,10 @@ describe('video.js', function () { it('catches invalid instream bids', function () { const bid = { - transactionId: 'au' + adUnitId: 'au' }; const adUnits = [{ - transactionId: 'au', + adUnitId: 'au', mediaTypes: { video: {context: 'instream'} } @@ -110,26 +110,26 @@ describe('video.js', function () { it('catches invalid bids when prebid-cache is disabled', function () { const adUnits = [{ - transactionId: 'au', + adUnitId: 'au', bidder: 'vastOnlyVideoBidder', mediaTypes: {video: {}}, }]; - const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); + const valid = isValidVideoBid({ adUnitId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(false); }); it('validates valid outstream bids', function () { const bid = { - transactionId: 'au', + adUnitId: 'au', renderer: { url: 'render.url', render: () => true, } }; const adUnits = [{ - transactionId: 'au', + adUnitId: 'au', mediaTypes: { video: {context: 'outstream'} } @@ -140,10 +140,10 @@ describe('video.js', function () { it('validates valid outstream bids with a publisher defined renderer', function () { const bid = { - transactionId: 'au', + adUnitId: 'au', }; const adUnits = [{ - transactionId: 'au', + adUnitId: 'au', mediaTypes: { video: { context: 'outstream', @@ -160,10 +160,10 @@ describe('video.js', function () { it('catches invalid outstream bids', function () { const bid = { - transactionId: 'au', + adUnitId: 'au', }; const adUnits = [{ - transactionId: 'au', + adUnitId: 'au', mediaTypes: { video: {context: 'outstream'} } 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..8d29c786595 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -128,10 +128,31 @@ 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, { + core: { + name: 'chunk-core', + test: (module) => { + return module.resource?.startsWith(core) || module.resource?.startsWith(nodeModules); + } + }, + 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 index 7279455e155..86f5f24d580 100644 --- a/webpack.creative.js +++ b/webpack.creative.js @@ -10,8 +10,14 @@ module.exports = { }, entry: { 'creative': { - import: './libraries/creativeRender/crossDomain.js', + 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'),