diff --git a/.eslintrc.js b/.eslintrc.js index fc3ad3afe66..511e78048e4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,12 +11,25 @@ module.exports = { node: { moduleDirectory: ['node_modules', './'] } + }, + 'jsdoc': { + mode: 'typescript', + tagNamePreference: { + 'tag constructor': 'constructor', + extends: 'extends', + method: 'method', + return: 'return', + } } }, - extends: 'standard', + extends: [ + 'standard', + 'plugin:jsdoc/recommended' + ], plugins: [ 'prebid', - 'import' + 'import', + 'jsdoc' ], globals: { 'BROWSERSTACK_USERNAME': false, @@ -46,6 +59,24 @@ module.exports = { 'no-undef': 2, 'no-useless-escape': 'off', 'no-console': 'error', + 'jsdoc/check-types': 'off', + 'jsdoc/newline-after-description': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-param-name': 'off', + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-property': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-property-name': 'off', + 'jsdoc/require-property-type': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-returns-check': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/require-yields': 'off', + 'jsdoc/require-yields-check': 'off', + 'jsdoc/tag-lines': 'off' }, overrides: Object.keys(allowedModules).map((key) => ({ files: key + '/**/*.js', diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 584f6f8894a..3bee8f7c947 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -70,4 +70,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index 69cf4c5fc7f..b5c59c85160 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 45ca30a7a3d..9deac9963fb 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -55,7 +55,6 @@ Follow steps above for general review process. In addition, please verify the fo - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - The bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): - bcat, battr, badv diff --git a/features.json b/features.json index ccb2166a05f..4d8377cda7d 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,5 @@ [ "NATIVE", - "VIDEO" + "VIDEO", + "UID2_CSTG" ] diff --git a/gulpfile.js b/gulpfile.js index 09de874e389..5e16af8b0c1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -158,6 +158,20 @@ function makeWebpackPkg(extraConfig = {}) { } } +function buildCreative() { + return gulp.src(['**/*']) + .pipe(webpackStream(require('./webpack.creative.js'))) + .pipe(gulp.dest('build/creative')) +} + +function updateCreativeExample(cb) { + const CREATIVE_EXAMPLE = 'integrationExamples/gpt/x-domain/creative.html'; + const root = require('node-html-parser').parse(fs.readFileSync(CREATIVE_EXAMPLE)); + root.querySelectorAll('script')[0].textContent = fs.readFileSync('build/creative/creative.js') + fs.writeFileSync(CREATIVE_EXAMPLE, root.toString()) + cb(); +} + function getModulesListToAddInBanner(modules) { if (!modules || modules.length === helpers.getModuleNames().length) { return 'All available modules for this version.' @@ -405,6 +419,7 @@ function watchTaskMaker(options = {}) { return function watch(done) { var mainWatcher = gulp.watch([ 'src/**/*.js', + 'libraries/**/*.js', 'modules/**/*.js', ].concat(options.alsoWatch)); @@ -415,8 +430,8 @@ function watchTaskMaker(options = {}) { } } -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')}); +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)}); // support tasks gulp.task(lint); @@ -447,21 +462,23 @@ 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.series('test-all-features-disabled', 'test-only'))); +gulp.task('test', gulp.series(clean, lint, gulp.parallel('build-creative', gulp.series('test-all-features-disabled', 'test-only')))); gulp.task('test-coverage', gulp.series(clean, testCoverage)); gulp.task(viewCoverage); gulp.task('coveralls', gulp.series('test-coverage', coveralls)); -gulp.task('build', gulp.series(clean, 'build-bundle-prod')); +gulp.task('build', gulp.series(clean, 'build-bundle-prod', 'build-creative')); 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', watchFast))); +gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', buildCreative, 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))); diff --git a/integrationExamples/gpt/adnuntius_example.html b/integrationExamples/gpt/adnuntius_example.html new file mode 100644 index 00000000000..b61c4e0674e --- /dev/null +++ b/integrationExamples/gpt/adnuntius_example.html @@ -0,0 +1,95 @@ + + + + + + + +

Adnuntius Prebid Adaptor Test

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

Contxtful RTD Provider

+
+ + + + \ No newline at end of file diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index 47ba5b8f18a..03a2356f0ef 100644 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -8,6 +8,7 @@ --> + @@ -19,9 +20,10 @@ code: 'div-gpt-ad-1460505748561-0', mediaTypes: { banner: { - sizes: [[300, 250], [300,600]], + sizes: [[300, 250]], } }, + // Replace this object to test a new Adapter! bids: [{ bidder: 'appnexus', @@ -40,12 +42,13 @@ - +

Prebid.js Test

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

Prebid.js TPMN Banner Example

+ +
+

Prebid.js TPMN Video Example

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

Ad Serverless Test Page

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

Prebid Native

+
+
+ +
+
+ + + + diff --git a/libraries/appnexusKeywords/anKeywords.js b/libraries/appnexusUtils/anKeywords.js similarity index 91% rename from libraries/appnexusKeywords/anKeywords.js rename to libraries/appnexusUtils/anKeywords.js index 5dc0b453253..d6714dacc21 100644 --- a/libraries/appnexusKeywords/anKeywords.js +++ b/libraries/appnexusUtils/anKeywords.js @@ -1,4 +1,4 @@ -import {_each, deepAccess, getValueString, isArray, isStr, mergeDeep, isNumber} from '../../src/utils.js'; +import {_each, deepAccess, isArray, isNumber, isStr, mergeDeep, logWarn} from '../../src/utils.js'; import {getAllOrtbKeywords} from '../keywords/keywords.js'; import {CLIENT_SECTIONS} from '../../src/fpd/oneClient.js'; @@ -12,6 +12,19 @@ const ORTB_SEG_PATHS = ['user.data'].concat( CLIENT_SECTIONS.map((prefix) => `${prefix}.content.data`) ); +function getValueString(param, val, defaultValue) { + if (val === undefined || val === null) { + return defaultValue; + } + if (isStr(val)) { + return val; + } + if (isNumber(val)) { + return val.toString(); + } + logWarn('Unsuported type for param: ' + param + ' required type: String'); +} + /** * Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties * normally read from bidder params diff --git a/libraries/appnexusUtils/anUtils.js b/libraries/appnexusUtils/anUtils.js new file mode 100644 index 00000000000..9b55cd5c2a4 --- /dev/null +++ b/libraries/appnexusUtils/anUtils.js @@ -0,0 +1,25 @@ +/** + * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' + * @param {string} value string value to convert + */ +import {deepClone, isPlainObject} from '../../src/utils.js'; + +export function convertCamelToUnderscore(value) { + return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { + return '_' + y.toLowerCase(); + }).replace(/^_/, ''); +} + +/** + * Creates an array of n length and fills each item with the given value + */ +export function fill(value, length) { + let newArray = []; + + for (let i = 0; i < length; i++) { + let valueToPush = isPlainObject(value) ? deepClone(value) : value; + newArray.push(valueToPush); + } + + return newArray; +} diff --git a/libraries/chunk/chunk.js b/libraries/chunk/chunk.js new file mode 100644 index 00000000000..57be7bd5016 --- /dev/null +++ b/libraries/chunk/chunk.js @@ -0,0 +1,19 @@ +/** + * http://npm.im/chunk + * Returns an array with *size* chunks from given array + * + * Example: + * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => + * [['a', 'b'], ['c', 'd'], ['e']] + */ +export function chunk(array, size) { + let newArray = []; + + for (let i = 0; i < Math.ceil(array.length / size); i++) { + let start = i * size; + let end = start + size; + newArray.push(array.slice(start, end)); + } + + return newArray; +} diff --git a/libraries/cmp/cmpClient.js b/libraries/cmp/cmpClient.js index 03a50c37bb3..1d0b327cee4 100644 --- a/libraries/cmp/cmpClient.js +++ b/libraries/cmp/cmpClient.js @@ -4,46 +4,46 @@ import {GreedyPromise} from '../../src/utils/promise.js'; * @typedef {function} CMPClient * * @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}. - * @param {bool} once if true, discard cross-frame event listeners once a reply message is received. + * @param {boolean} once if true, discard cross-frame event listeners once a reply message is received. * @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined. * @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required) * @property {() => void} close close the client; currently, this just stops listening for cross-frame messages. */ +export const MODE_MIXED = 0; +export const MODE_RETURN = 1; +export const MODE_CALLBACK = 2; + /** * Returns a client function that can interface with a CMP regardless of where it's located. * - * @param apiName name of the CMP api, e.g. "__gpp" - * @param apiVersion? CMP API version - * @param apiArgs? names of the arguments taken by the api function, in order. - * @param callbackArgs? names of the cross-frame response payload properties that should be passed as callback arguments, in order - * @param mode? controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved. + * @param {object} obj + * @param obj.apiName name of the CMP api, e.g. "__gpp" + * @param [obj.apiVersion] CMP API version + * @param [obj.apiArgs] names of the arguments taken by the api function, in order. + * @param [obj.callbackArgs] names of the cross-frame response payload properties that should be passed as callback arguments, in order + * @param [obj.mode] controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved. * - * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these - * cases "subscriptions" and "one-shot calls" respectively: + * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these + * cases "subscriptions" and "one-shot calls" respectively: * - * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback - * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and - * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's - * return value when it's directly accessible, or with the result from the first (and, presumably, the only) - * cross-frame reply when it's not; + * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback + * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and + * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's + * return value when it's directly accessible, or with the result from the first (and, presumably, the only) + * cross-frame reply when it's not; * - * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined - * when cross-frame; + * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined + * when cross-frame; * - * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead, - * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will - * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve - * the returned promise. Subscriptions are treated in the same way as MODE_MIXED. + * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead, + * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will + * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve + * the returned promise. Subscriptions are treated in the same way as MODE_MIXED. * * @param win * @returns {CMPClient} CMP invocation function (or null if no CMP was found). */ - -export const MODE_MIXED = 0; -export const MODE_RETURN = 1; -export const MODE_CALLBACK = 2; - export function cmpClient( { apiName, diff --git a/libraries/creativeRender/constants.js b/libraries/creativeRender/constants.js new file mode 100644 index 00000000000..7b67f8ed5cd --- /dev/null +++ b/libraries/creativeRender/constants.js @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000000..ffa8b468f12 --- /dev/null +++ b/libraries/creativeRender/crossDomain.js @@ -0,0 +1,57 @@ +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 new file mode 100644 index 00000000000..19d34e16844 --- /dev/null +++ b/libraries/creativeRender/direct.js @@ -0,0 +1,62 @@ +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 new file mode 100644 index 00000000000..80bb0592a1f --- /dev/null +++ b/libraries/creativeRender/writer.js @@ -0,0 +1,34 @@ +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/currencyUtils/currency.js b/libraries/currencyUtils/currency.js new file mode 100644 index 00000000000..924f8f200d8 --- /dev/null +++ b/libraries/currencyUtils/currency.js @@ -0,0 +1,31 @@ +import {getGlobal} from '../../src/prebidGlobal.js'; +import {keyCompare} from '../../src/utils/reducers.js'; + +/** + * Attempt to convert `amount` from the currency `fromCur` to the currency `toCur`. + * + * By default, when the conversion is not possible (currency module not present or + * throwing errors), the amount is returned unchanged. This behavior can be + * toggled off with bestEffort = false. + */ +export function convertCurrency(amount, fromCur, toCur, bestEffort = true) { + if (fromCur === toCur) return amount; + let result = amount; + try { + result = getGlobal().convertCurrency(amount, fromCur, toCur); + } catch (e) { + if (!bestEffort) throw e; + } + return result; +} + +export function currencyNormalizer(toCurrency = null, bestEffort = true, convert = convertCurrency) { + return function (amount, currency) { + if (toCurrency == null) toCurrency = currency; + return convert(amount, currency, toCurrency, bestEffort); + } +} + +export function currencyCompare(get = (obj) => [obj.cpm, obj.currency], normalize = currencyNormalizer()) { + return keyCompare(obj => normalize.apply(null, get(obj))) +} diff --git a/libraries/gptUtils/gptUtils.js b/libraries/gptUtils/gptUtils.js new file mode 100644 index 00000000000..950f28c618f --- /dev/null +++ b/libraries/gptUtils/gptUtils.js @@ -0,0 +1,37 @@ +import {find} from '../../src/polyfill.js'; +import {compareCodeAndSlot, isGptPubadsDefined} from '../../src/utils.js'; + +/** + * Returns filter function to match adUnitCode in slot + * @param {string} adUnitCode AdUnit code + * @return {function} filter function + */ +export function isSlotMatchingAdUnitCode(adUnitCode) { + return (slot) => compareCodeAndSlot(slot, adUnitCode); +} + +/** + * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page + */ +export function getGptSlotForAdUnitCode(adUnitCode) { + let matchingSlot; + if (isGptPubadsDefined()) { + // find the first matching gpt slot on the page + matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); + } + return matchingSlot; +} + +/** + * @summary Uses the adUnit's code in order to find a matching gptSlot on the page + */ +export function getGptSlotInfoForAdUnitCode(adUnitCode) { + const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); + if (matchingSlot) { + return { + gptSlot: matchingSlot.getAdUnitPath(), + divId: matchingSlot.getSlotElementId() + }; + } + return {}; +} diff --git a/libraries/htmlEscape/htmlEscape.js b/libraries/htmlEscape/htmlEscape.js new file mode 100644 index 00000000000..f0952c02e3c --- /dev/null +++ b/libraries/htmlEscape/htmlEscape.js @@ -0,0 +1,26 @@ +/** + * Encode a string for inclusion in HTML. + * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and + * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ + * @return {string} + */ +export const escapeUnsafeChars = (() => { + const escapes = { + '<': '\\u003C', + '>': '\\u003E', + '/': '\\u002F', + '\\': '\\\\', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\0', + '\u2028': '\\u2028', + '\u2029': '\\u2029' + }; + + return function (str) { + return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]); + }; +})(); diff --git a/libraries/ortbConverter/README.md b/libraries/ortbConverter/README.md index 31f56b4c754..751971eebdc 100644 --- a/libraries/ortbConverter/README.md +++ b/libraries/ortbConverter/README.md @@ -80,8 +80,7 @@ However, there are two restrictions (to avoid them, use the [other customization ) ``` - -### Fine grained customization - imp, request, bidResponse, response +### Fine grained customization - imp, request, bidResponse, response When invoked, `toORTB({bidRequests, bidderRequest})` first loops through each request in `bidRequests`, converting them into ORTB `imp` objects. It then packages them into a single ORTB request, adding other parameters that are not imp-specific (such as for example `request.tmax`). @@ -91,7 +90,7 @@ a single return value. You can customize each of these steps using the `ortbConverter` arguments `imp`, `request`, `bidResponse` and `response`: -### Customizing imps: `imp(buildImp, bidRequest, context)` +### Customizing imps: `imp(buildImp, bidRequest, context)` Invoked once for each input `bidRequest`; should return the ORTB `imp` object to include in the request. The arguments are: @@ -101,7 +100,7 @@ The arguments are: - `context`: a [context object](#context) that contains at least: - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`. -#### Example: attaching custom bid params +#### Example: attaching custom bid params ```javascript const converter = ortbConverter({ @@ -351,7 +350,7 @@ const converter = ortbConverter({ - the `context` argument of `ortbConverter`: e.g. `ortbConverter({context: {ttl: 30}})`. This will set `context.ttl = 30` globally for the converter. - the `context` argument of `toORTB`: e.g. `converter.toORTB({bidRequests, bidderRequest, context: {ttl: 30}})`. This will set `context.ttl = 30` only for this request. -### Special `context` properties +### Special `context` properties For ease of use, the conversion logic gives special meaning to some context properties: diff --git a/libraries/sizeUtils/sizeUtils.js b/libraries/sizeUtils/sizeUtils.js new file mode 100644 index 00000000000..41cdd71df89 --- /dev/null +++ b/libraries/sizeUtils/sizeUtils.js @@ -0,0 +1,29 @@ +/** + * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) + * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` + * @param {object} adUnit one adUnit object from the normal list of adUnits + * @returns {Array.} array of arrays containing numeric sizes + */ +export function getAdUnitSizes(adUnit) { + if (!adUnit) { + return; + } + + let sizes = []; + if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { + let bannerSizes = adUnit.mediaTypes.banner.sizes; + if (Array.isArray(bannerSizes[0])) { + sizes = bannerSizes; + } else { + sizes.push(bannerSizes); + } + // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders + } else if (Array.isArray(adUnit.sizes)) { + if (Array.isArray(adUnit.sizes[0])) { + sizes = adUnit.sizes; + } else { + sizes.push(adUnit.sizes); + } + } + return sizes; +} diff --git a/libraries/transformParamsUtils/convertTypes.js b/libraries/transformParamsUtils/convertTypes.js new file mode 100644 index 00000000000..813d8e6e693 --- /dev/null +++ b/libraries/transformParamsUtils/convertTypes.js @@ -0,0 +1,36 @@ +import {isFn} from '../../src/utils.js'; + +/** + * Try to convert a value to a type. + * If it can't be done, the value will be returned. + * + * @param {string} typeToConvert The target type. e.g. "string", "number", etc. + * @param {*} value The value to be converted into typeToConvert. + */ +function tryConvertType(typeToConvert, value) { + if (typeToConvert === 'string') { + return value && value.toString(); + } else if (typeToConvert === 'number') { + return Number(value); + } else { + return value; + } +} + +export function convertTypes(types, params) { + Object.keys(types).forEach(key => { + if (params[key]) { + if (isFn(types[key])) { + params[key] = types[key](params[key]); + } else { + params[key] = tryConvertType(types[key], params[key]); + } + + // don't send invalid values + if (isNaN(params[key])) { + delete params.key; + } + } + }); + return params; +} diff --git a/libraries/uid2Eids/uid2Eids.js b/libraries/uid2Eids/uid2Eids.js new file mode 100644 index 00000000000..ce4f4fa3b2a --- /dev/null +++ b/libraries/uid2Eids/uid2Eids.js @@ -0,0 +1,14 @@ +export const UID2_EIDS = { + 'uid2': { + source: 'uidapi.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } +} diff --git a/libraries/urlUtils/urlUtils.js b/libraries/urlUtils/urlUtils.js new file mode 100644 index 00000000000..f0c5823aab1 --- /dev/null +++ b/libraries/urlUtils/urlUtils.js @@ -0,0 +1,7 @@ +export function tryAppendQueryString(existingUrl, key, value) { + if (value) { + return existingUrl + key + '=' + encodeURIComponent(value) + '&'; + } + + return existingUrl; +} diff --git a/modules/.submodules.json b/modules/.submodules.json index fdc79c8b868..d2a13a57330 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -47,7 +47,8 @@ "adqueryIdSystem", "gravitoIdSystem", "freepassIdSystem", - "operaadsIdSystem" + "operaadsIdSystem", + "mygaruIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -57,29 +58,42 @@ "1plusXRtdProvider", "a1MediaRtdProvider", "aaxBlockmeterRtdProvider", + "adlooxRtdProvider", + "adnuntiusRtdProvider", "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", "blueconicRtdProvider", + "brandmetricsRtdProvider", "browsiRtdProvider", "captifyRtdProvider", + "mediafilterRtdProvider", "confiantRtdProvider", "dgkeywordRtdProvider", + "experianRtdProvider", "geoedgeRtdProvider", + "geolocationRtdProvider", + "greenbidsRtdProvider", + "growthCodeRtdProvider", "hadronRtdProvider", - "haloRtdProvider", "iasRtdProvider", + "idWardRtdProvider", + "imRtdProvider", + "intersectionRtdProvider", "jwplayerRtdProvider", "medianetRtdProvider", "mgidRtdProvider", + "neuwoRtdProvider", "oneKeyRtdProvider", "optimeraRtdProvider", + "oxxionRtdProvider", "permutiveRtdProvider", + "qortexRtdProvider", "reconciliationRtdProvider", + "relevadRtdProvider", "sirdataRtdProvider", "timeoutRtdProvider", - "weboramaRtdProvider", - "zeusPrimeRtdProvider" + "weboramaRtdProvider" ], "fpdModule": [ "validationFpdModule", diff --git a/modules/33acrossAnalyticsAdapter.js b/modules/33acrossAnalyticsAdapter.js new file mode 100644 index 00000000000..e3539906b13 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.js @@ -0,0 +1,656 @@ +import { deepAccess, logInfo, logWarn, logError, deepClone } from '../src/utils.js'; +import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager, { coppaDataHandler, gdprDataHandler, gppDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; + +/** + * @typedef {typeof import('../src/constants.json').EVENTS} EVENTS + */ +const { EVENTS } = CONSTANTS; + +/** @typedef {'pending'|'available'|'targetingSet'|'rendered'|'timeout'|'rejected'|'noBid'|'error'} BidStatus */ +/** + * @type {Object} + */ +const BidStatus = { + PENDING: 'pending', + AVAILABLE: 'available', + TARGETING_SET: 'targetingSet', + RENDERED: 'rendered', + TIMEOUT: 'timeout', + REJECTED: 'rejected', + NOBID: 'noBid', + ERROR: 'error', +} + +const ANALYTICS_VERSION = '1.0.0'; +const PROVIDER_NAME = '33across'; +const GVLID = 58; +/** Time to wait for all transactions in an auction to complete before sending the report */ +const DEFAULT_TRANSACTION_TIMEOUT = 10000; +/** Time to wait after all GAM slots have registered before sending the report */ +export const POST_GAM_TIMEOUT = 500; +export const DEFAULT_ENDPOINT = 'https://analytics.33across.com/api/v1/event'; + +export const log = getLogger(); + +/** + * @typedef {Object} AnalyticsReport - Sent when all bids are complete (as determined by `bidWon` and `slotRenderEnded` events) + * @property {string} analyticsVersion - Version of the Prebid.js 33Across Analytics Adapter + * @property {string} pid - Partner ID + * @property {string} src - Source of the report ('pbjs') + * @property {string} pbjsVersion - Version of Prebid.js + * @property {Auction[]} auctions + */ + +/** + * @typedef {Object} AnalyticsCache + * @property {string} pid Partner ID + * @property {Object} auctions + * @property {string} [usPrivacy] + */ + +/** + * @typedef {Object} Auction - Parsed auction data + * @property {AdUnit[]} adUnits + * @property {string} auctionId + * @property {string[]} userIds + */ + +/** + * @typedef {`${number}x${number}`} AdUnitSize + */ + +/** + * @typedef {('banner'|'native'|'video')} AdUnitMediaType + */ + +/** + * @typedef {Object} BidResponse + * @property {number} cpm + * @property {string} cur + * @property {number} [cpmOrig] + * @property {number} cpmFloor + * @property {AdUnitMediaType} mediaType + * @property {AdUnitSize} size + */ + +/** + * @typedef {Object} Bid - Parsed bid data + * @property {string} bidder + * @property {string} bidId + * @property {string} source + * @property {string} status + * @property {BidResponse} [bidResponse] + * @property {1|0} [hasWon] + */ + +/** + * @typedef {Object} AdUnit - Parsed adUnit data + * @property {string} transactionId - Primary key for *this* auction/adUnit combination + * @property {string} adUnitCode + * @property {string} slotId - Equivalent to GPID. (Note that + * GPID supports adUnits where multiple units have the same `code` values + * by appending a `#UNIQUIFIER`. The value of the UNIQUIFIER is likely to be the div-id, + * but, if div-id is randomized / unavailable, may be something else like the media size) + * @property {Array} mediaTypes + * @property {Array} sizes + * @property {Array} bids + */ + +/** + * After the first transaction begins, wait until all transactions are complete + * before calling `onComplete`. If the timeout is reached before all transactions + * are complete, send the report anyway. + * + * Use this to track all transactions per auction, and send the report as soon + * as all adUnits have been won (or after timeout) even if other bid/auction + * activity is still happening. + */ +class TransactionManager { + /** + * Milliseconds between activity to allow until this collection automatically completes. + * @type {number} + */ + #sendTimeout; + #sendTimeoutId; + #transactionsPending = new Set(); + #transactionsCompleted = new Set(); + #onComplete; + + constructor({ timeout, onComplete }) { + this.#sendTimeout = timeout; + this.#onComplete = onComplete; + } + + status() { + return { + pending: [...this.#transactionsPending], + completed: [...this.#transactionsCompleted], + }; + } + + initiate(transactionId) { + this.#transactionsPending.add(transactionId); + this.#restartSendTimeout(); + } + + complete(transactionId) { + if (!this.#transactionsPending.has(transactionId)) { + log.warn(`transactionId "${transactionId}" was not found. No transaction to mark as complete.`); + return; + } + + this.#transactionsPending.delete(transactionId); + this.#transactionsCompleted.add(transactionId); + + if (this.#transactionsPending.size === 0) { + this.#flushTransactions(); + } + } + + #flushTransactions() { + this.#clearSendTimeout(); + this.#transactionsPending = new Set(); + this.#onComplete(); + } + + // gulp-eslint is using eslint 6, a version that doesn't support private method syntax + // eslint-disable-next-line no-dupe-class-members + #clearSendTimeout() { + return clearTimeout(this.#sendTimeoutId); + } + + // eslint-disable-next-line no-dupe-class-members + #restartSendTimeout() { + this.#clearSendTimeout(); + + this.#sendTimeoutId = setTimeout(() => { + if (this.#sendTimeout !== 0) { + log.warn(`Timed out waiting for ad transactions to complete. Sending report.`); + } + + this.#flushTransactions(); + }, this.#sendTimeout); + } +} + +/** + * Initialized during `enableAnalytics`. Exported for testing purposes. + */ +export const locals = { + /** @type {Object} - one manager per auction */ + transactionManagers: {}, + /** @type {AnalyticsCache} */ + cache: { + auctions: {}, + pid: '', + }, + /** @type {Object} */ + adUnitMap: {}, + reset() { + this.transactionManagers = {}; + this.cache = { + auctions: {}, + pid: '', + }; + this.adUnitMap = {}; + } +} + +/** + * @typedef {Object} AnalyticsAdapter + * @property {function} track + * @property {function} enableAnalytics + * @property {function} disableAnalytics + * @property {function} [originEnableAnalytics] + * @property {function} [originDisableAnalytics] + * @property {function} [_oldEnable] + */ + +/** + * @type {AnalyticsAdapter} + */ +const analyticsAdapter = Object.assign( + buildAdapter({ analyticsType: 'endpoint' }), + { track: analyticEventHandler } +); + +analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics; +analyticsAdapter.enableAnalytics = enableAnalyticsWrapper; + +/** + * @typedef {Object} AnalyticsConfig + * @property {string} provider - set by pbjs at module registration time + * @property {Object} options + * @property {string} options.pid - Publisher/Partner ID + * @property {string} [options.endpoint=DEFAULT_ENDPOINT] - Endpoint to send analytics data + * @property {number} [options.timeout=DEFAULT_TRANSACTION_TIMEOUT] - Timeout for sending analytics data + */ + +/** + * @param {AnalyticsConfig} config Analytics module configuration + */ +function enableAnalyticsWrapper(config) { + const { options } = config; + + const pid = options.pid; + if (!pid) { + log.error('No partnerId provided for "options.pid". No analytics will be sent.'); + + return; + } + + const endpoint = calculateEndpoint(options.endpoint); + this.getUrl = () => endpoint; + + const timeout = calculateTransactionTimeout(options.timeout); + this.getTimeout = () => timeout; + + locals.cache = { + pid, + auctions: {}, + }; + + window.googletag = window.googletag || { cmd: [] }; + window.googletag.cmd.push(subscribeToGamSlots); + + analyticsAdapter.originEnableAnalytics(config); +} + +/** + * @param {string} [endpoint] + * @returns {string} + */ +function calculateEndpoint(endpoint = DEFAULT_ENDPOINT) { + if (typeof endpoint === 'string' && endpoint.startsWith('http')) { + return endpoint; + } + + log.info(`Invalid endpoint provided for "options.endpoint". Using default endpoint.`); + + return DEFAULT_ENDPOINT; +} +/** + * @param {number} [configTimeout] + * @returns {number} Transaction Timeout + */ +function calculateTransactionTimeout(configTimeout = DEFAULT_TRANSACTION_TIMEOUT) { + if (typeof configTimeout === 'number' && configTimeout >= 0) { + return configTimeout; + } + + log.info(`Invalid timeout provided for "options.timeout". Using default timeout of ${DEFAULT_TRANSACTION_TIMEOUT}ms.`); + + return DEFAULT_TRANSACTION_TIMEOUT; +} + +function subscribeToGamSlots() { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + setTimeout(() => { + const { transactionId, auctionId } = + getAdUnitMetadata(event.slot.getAdUnitPath(), event.slot.getSlotElementId()); + if (!transactionId || !auctionId) { + const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; + log.warn('Could not find configured ad unit matching GAM render of slot:', { slotName }); + return; + } + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); + }, POST_GAM_TIMEOUT); + }); +} + +function getAdUnitMetadata(adUnitPath, adSlotElementId) { + const adUnitMeta = locals.adUnitMap[adUnitPath] || locals.adUnitMap[adSlotElementId]; + if (adUnitMeta && adUnitMeta.length > 0) { + return adUnitMeta[adUnitMeta.length - 1]; + } + return {}; +} + +/** necessary for testing */ +analyticsAdapter.originDisableAnalytics = analyticsAdapter.disableAnalytics; +analyticsAdapter.disableAnalytics = function () { + analyticsAdapter._oldEnable = enableAnalyticsWrapper; + locals.reset(); + analyticsAdapter.originDisableAnalytics(); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: analyticsAdapter, + code: PROVIDER_NAME, + gvlid: GVLID, +}); + +export default analyticsAdapter; + +/** + * @param {AnalyticsCache} analyticsCache + * @param {string} completedAuctionId value of auctionId + * @return {AnalyticsReport} Analytics report + */ +function createReportFromCache(analyticsCache, completedAuctionId) { + const { pid, auctions } = analyticsCache; + + const report = { + pid, + src: 'pbjs', + analyticsVersion: ANALYTICS_VERSION, + pbjsVersion: '$prebid.version$', // Replaced by build script + auctions: [ auctions[completedAuctionId] ], + } + if (uspDataHandler.getConsentData()) { + report.usPrivacy = uspDataHandler.getConsentData(); + } + + if (gdprDataHandler.getConsentData()) { + report.gdpr = Number(Boolean(gdprDataHandler.getConsentData().gdprApplies)); + report.gdprConsent = gdprDataHandler.getConsentData().consentString || ''; + } + + if (gppDataHandler.getConsentData()) { + report.gpp = gppDataHandler.getConsentData().gppString; + report.gppSid = gppDataHandler.getConsentData().applicableSections; + } + + if (coppaDataHandler.getCoppa()) { + report.coppa = Number(coppaDataHandler.getCoppa()); + } + + return report; +} + +function getCachedBid(auctionId, bidId) { + const auction = locals.cache.auctions[auctionId]; + for (let adUnit of auction.adUnits) { + for (let bid of adUnit.bids) { + if (bid.bidId === bidId) { + return bid; + } + } + } + log.error(`Cannot find bid "${bidId}" in auction "${auctionId}".`); +}; + +/** + * @param {Object} args + * @param {Object} args.args Event data + * @param {EVENTS[keyof EVENTS]} args.eventType + */ +function analyticEventHandler({ eventType, args }) { + if (!locals.cache) { + log.error('Something went wrong. Analytics cache is not initialized.'); + return; + } + + switch (eventType) { + case EVENTS.AUCTION_INIT: + onAuctionInit(args); + break; + case EVENTS.BID_REQUESTED: // BidStatus.PENDING + onBidRequested(args); + break; + case EVENTS.BID_TIMEOUT: + for (let bid of args) { + setCachedBidStatus(bid.auctionId, bid.bidId, BidStatus.TIMEOUT); + } + break; + case EVENTS.BID_RESPONSE: + onBidResponse(args); + break; + case EVENTS.BID_REJECTED: + onBidRejected(args); + break; + case EVENTS.NO_BID: + case EVENTS.SEAT_NON_BID: + setCachedBidStatus(args.auctionId, args.bidId, BidStatus.NOBID); + break; + case EVENTS.BIDDER_ERROR: + if (args.bidderRequest && args.bidderRequest.bids) { + for (let bid of args.bidderRequest.bids) { + setCachedBidStatus(args.bidderRequest.auctionId, bid.bidId, BidStatus.ERROR); + } + } + break; + case EVENTS.AUCTION_END: + onAuctionEnd(args); + break; + case EVENTS.BID_WON: // BidStatus.TARGETING_SET | BidStatus.RENDERED | BidStatus.ERROR + onBidWon(args); + break; + default: + break; + } +} + +/**************** + * AUCTION_INIT * + ***************/ +function onAuctionInit({ adUnits, auctionId, bidderRequests }) { + if (typeof auctionId !== 'string' || !Array.isArray(bidderRequests)) { + log.error('Analytics adapter failed to parse auction.'); + return; + } + + locals.cache.auctions[auctionId] = { + auctionId, + adUnits: adUnits.map(au => { + setAdUnitMap(au.code, auctionId, au.transactionId); + + return { + transactionId: au.transactionId, + adUnitCode: au.code, + // Note: GPID supports adUnits that have matching `code` values by appending a `#UNIQUIFIER`. + // The value of the UNIQUIFIER is likely to be the div-id, + // but, if div-id is randomized / unavailable, may be something else like the media size) + slotId: deepAccess(au, 'ortb2Imp.ext.gpid') || deepAccess(au, 'ortb2Imp.ext.data.pbadslot', au.code), + mediaTypes: Object.keys(au.mediaTypes), + sizes: au.sizes.map(size => size.join('x')), + bids: [], + } + }), + userIds: Object.keys(deepAccess(bidderRequests, '0.bids.0.userId', {})), + }; + + locals.transactionManagers[auctionId] ||= + new TransactionManager({ + timeout: analyticsAdapter.getTimeout(), + onComplete() { + sendReport( + createReportFromCache(locals.cache, auctionId), + analyticsAdapter.getUrl() + ); + delete locals.transactionManagers[auctionId]; + } + }); +} + +function setAdUnitMap(adUnitCode, auctionId, transactionId) { + if (!locals.adUnitMap[adUnitCode]) { + locals.adUnitMap[adUnitCode] = []; + } + + locals.adUnitMap[adUnitCode].push({ auctionId, transactionId }); +} + +/***************** + * BID_REQUESTED * + ****************/ +function onBidRequested({ auctionId, bids }) { + for (let { bidder, bidId, transactionId, src } of bids) { + const auction = locals.cache.auctions[auctionId]; + const adUnit = auction.adUnits.find(adUnit => adUnit.transactionId === transactionId); + if (!adUnit) return; + adUnit.bids.push({ + bidder, + bidId, + status: BidStatus.PENDING, + hasWon: 0, + source: src, + }); + + // if there is no manager for this auction, then the auction has already been completed + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].initiate(transactionId); + } +} + +/**************** + * BID_RESPONSE * + ***************/ +function onBidResponse({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, size, status, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, status); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size + }, + source + } + ); +} + +/**************** + * BID_REJECTED * + ***************/ +function onBidRejected({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, width, height, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, BidStatus.REJECTED); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size: `${width}x${height}` + }, + source + } + ); +} + +/*************** + * AUCTION_END * + **************/ +/** + * @param {Object} args + * @param {{requestId: string, status: string}[]} args.bidsReceived + * @param {string} args.auctionId + * @returns {void} + */ +function onAuctionEnd({ bidsReceived, auctionId }) { + for (let bid of bidsReceived) { + setCachedBidStatus(auctionId, bid.requestId, bid.status); + } +} + +/*********** + * BID_WON * + **********/ +function onBidWon(bidWon) { + const { auctionId, requestId, transactionId } = bidWon; + const bid = getCachedBid(auctionId, requestId); + if (!bid) { + return; + } + + setBidStatus(bid, bidWon.status ?? BidStatus.ERROR); + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); +} + +/** + * @param {Bid} bid + * @param {BidStatus} [status] + * @returns {void} + */ +function setBidStatus(bid, status = BidStatus.AVAILABLE) { + const statusStates = { + pending: { + next: [BidStatus.AVAILABLE, BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + available: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + targetingSet: { + next: [BidStatus.RENDERED, BidStatus.ERROR, BidStatus.TIMEOUT], + }, + rendered: { + next: [], + }, + timeout: { + next: [], + }, + rejected: { + next: [], + }, + noBid: { + next: [], + }, + error: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + } + + const winningStatuses = [BidStatus.RENDERED]; + + if (statusStates[bid.status].next.includes(status)) { + bid.status = status; + if (winningStatuses.includes(status)) { + // occassionally we can detect a bidWon before prebid reports it as such + bid.hasWon = 1; + } + } +} + +function setCachedBidStatus(auctionId, bidId, status) { + const bid = getCachedBid(auctionId, bidId); + if (!bid) return; + setBidStatus(bid, status); +} + +/** + * Guarantees sending of data without waiting for response, even after page is left/closed + * + * @param {AnalyticsReport} report Request payload + * @param {string} endpoint URL + */ +function sendReport(report, endpoint) { + if (navigator.sendBeacon(endpoint, JSON.stringify(report))) { + log.info(`Analytics report sent to ${endpoint}`, report); + + return; + } + + log.error('Analytics report exceeded User-Agent data limits and was not sent.', report); +} + +/** + * Encapsulate certain logger functions and add a prefix to the final messages. + * + * @return {Object} New logger functions + */ +function getLogger() { + const LPREFIX = `${PROVIDER_NAME} Analytics: `; + + return { + info: (msg, ...args) => logInfo(`${LPREFIX}${msg}`, ...deepClone(args)), + warn: (msg, ...args) => logWarn(`${LPREFIX}${msg}`, ...deepClone(args)), + error: (msg, ...args) => logError(`${LPREFIX}${msg}`, ...deepClone(args)), + } +} diff --git a/modules/33acrossAnalyticsAdapter.md b/modules/33acrossAnalyticsAdapter.md new file mode 100644 index 00000000000..c56059e5526 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.md @@ -0,0 +1,76 @@ +# Overview + +```txt +Module Name: 33Across Analytics Adapter +Module Type: Analytics Adapter +Maintainer: analytics_support@33across.com +``` + +#### About + +This analytics adapter collects data about the performance of your ad slots +for each auction run on your site. It also provides insight into how identifiers +from the +[33Across User ID Sub-module](https://docs.prebid.org/dev-docs/modules/userid-submodules/33across.html) +and other user ID sub-modules improve your monetization. The data is sent at +the earliest opportunity for each auction to provide a more complete picture of +your ad performance. + +The analytics adapter is free to use! +However, the publisher must work with our account management team to obtain a +Publisher/Partner ID (PID) and enable Analytics for their account. +To get a PID and to have the publisher account enabled for Analytics, +you can reach out to our team at the following email - analytics_support@33across.com + +If you are an existing publisher and you already use a 33Across PID, +you can reach out to analytics_support@33across.com +to have your account enabled for analytics. + +The 33Across privacy policy is at . + +#### Analytics Options + +| Name | Scope | Example | Type | Description | +|-----------|----------|---------|----------|-------------| +| `pid` | required | abc123 | `string` | 33Across Publisher ID | +| `timeout` | optional | 10000 | `int` | Milliseconds to wait after last seen auction transaction before sending report (default 10000). | + +#### Configuration + +The data is sent at the earliest opportunity for each auction to provide +a more complete picture of your ad performance, even if the auction is interrupted +by a page navigation. At the latest, the adapter will always send the report +when the page is unloaded, at the end of the auction, or after the timeout, +whichever comes first. + +In order to guarantee consistent reports of your ad slot behavior, we recommend +including the GPT Pre-Auction Module, `gptPreAuction`. This module is included +by default when Prebid is downloaded. If you are compiling from source, +this might look something like: + +```sh +gulp bundle --modules=gptPreAuction,consentManagement,consentManagementGpp,consentManagementUsp,enrichmentFpdModule,gdprEnforcement,33acrossBidAdapter,33acrossIdSystem,33acrossAnalyticsAdapter +``` + +Enable the 33Across Analytics Adapter in Prebid.js using the analytics provider `33across` +and options as seen in the example below. + +#### Example Configuration + +```js +pbjs.enableAnalytics({ + provider: '33across', + options: { + /** + * The 33Across Publisher ID. + */ + pid: 'abc123', + /** + * Timeout in milliseconds after which an auction report + * will be sent regardless of auction state. + * [optional] + */ + timeout: 10000 + } +}); +``` diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index b965183de19..0e9beb22013 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -6,7 +6,6 @@ import { getWindowTop, isArray, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logWarn, mergeDeep, @@ -14,6 +13,7 @@ import { uniques } from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; // **************************** UTILS *************************** // const BIDDER_CODE = '33across'; diff --git a/modules/a1MediaBidAdapter.js b/modules/a1MediaBidAdapter.js new file mode 100644 index 00000000000..d640bbfe2d7 --- /dev/null +++ b/modules/a1MediaBidAdapter.js @@ -0,0 +1,104 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { replaceAuctionPrice } from '../src/utils.js'; + +const BIDDER_CODE = 'a1media'; +const END_POINT = 'https://d11.contentsfeed.com/dsp/breq/a1'; +const DEFAULT_CURRENCY = 'JPY'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + if (bidRequest.params.battr) { + Object.keys(bidRequest.mediaTypes).forEach(mType => { + imp[mType].battr = bidRequest.params.battr; + }) + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + if (!request.cur) { + request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + } + if (bid.params.bcat) { + request.bcat = bid.params.bcat; + } + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + + let resMediaType; + const reqMediaTypes = Object.keys(bidRequest.mediaTypes); + if (reqMediaTypes.length === 1) { + resMediaType = reqMediaTypes[0]; + } else { + if (bid.adm.search(/^(<\?xml| { + const parsedBid = seatbidItem.bid.map((bidItem) => ({ + ...bidItem, + adm: replaceAuctionPrice(bidItem.adm, bidItem.price), + nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price) + })); + return {...seatbidItem, bid: parsedBid}; + }); + + const responseBody = {...serverResponse.body, seatbid: parsedSeatbid}; + const bids = converter.fromORTB({ + response: responseBody, + request: bidRequest.data, + }).bids; + return bids; + }, + +}; +registerBidder(spec); diff --git a/modules/a1MediaBidAdapter.md b/modules/a1MediaBidAdapter.md new file mode 100644 index 00000000000..304b7e1bb5a --- /dev/null +++ b/modules/a1MediaBidAdapter.md @@ -0,0 +1,93 @@ +# Overview + +```markdown +Module Name: A1Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@a1mediagroup.co.kr +``` + +# Description + +Connects to A1Media exchange for bids. + +# Test Parameters + +## Sample Banner Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[320, 100]], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Video Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + mimes: ['video/mp4'], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Native Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + native: { + title: { + len: 140 + }, + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` diff --git a/modules/acuityAdsBidAdapter.js b/modules/acuityadsBidAdapter.js similarity index 94% rename from modules/acuityAdsBidAdapter.js rename to modules/acuityadsBidAdapter.js index b0bb132ddae..5b12eb2133b 100644 --- a/modules/acuityAdsBidAdapter.js +++ b/modules/acuityadsBidAdapter.js @@ -153,6 +153,15 @@ export const spec = { tmax: bidderRequest.timeout }; + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; diff --git a/modules/acuityAdsBidAdapter.md b/modules/acuityadsBidAdapter.md similarity index 98% rename from modules/acuityAdsBidAdapter.md rename to modules/acuityadsBidAdapter.md index a19e0a6b0ba..7f001cd9376 100644 --- a/modules/acuityAdsBidAdapter.md +++ b/modules/acuityadsBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: AcuityAds Bidder Adapter Module Type: AcuityAds Bidder Adapter -Maintainer: sa-support@brightcom.com +Maintainer: rafi.babler@acuityads.com ``` # Description diff --git a/modules/ad2ictionBidAdapter.js b/modules/ad2ictionBidAdapter.js new file mode 100644 index 00000000000..0f7cea45d14 --- /dev/null +++ b/modules/ad2ictionBidAdapter.js @@ -0,0 +1,59 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const BIDDER_CODE = 'ad2iction'; +export const SUPPORTED_AD_TYPES = [BANNER]; +export const API_ENDPOINT = 'https://ads.ad2iction.com/html/prebid/'; +export const API_VERSION_NUMBER = 3; +export const COOKIE_NAME = 'ad2udid'; + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + aliases: ['ad2'], + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: (bid) => { + return !!bid.params.id && typeof bid.params.id === 'string'; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const ids = validBidRequests.map((bid) => { + return { bannerId: bid.params.id, bidId: bid.bidId }; + }); + + const options = { + contentType: 'application/json', + withCredentials: false, + }; + + const udid = storage.cookiesAreEnabled() && storage.getCookie(COOKIE_NAME); + + const data = { + ids: JSON.stringify(ids), + ortb2: bidderRequest.ortb2, + refererInfo: bidderRequest.refererInfo, + v: API_VERSION_NUMBER, + udid: udid || '', + _: Math.round(new Date().getTime()), + }; + + return { + method: 'POST', + url: API_ENDPOINT, + data, + options, + }; + }, + interpretResponse: (serverResponse, bidRequest) => { + if (!Array.isArray(serverResponse.body)) { + return []; + } + + const bidResponses = serverResponse.body; + + return bidResponses; + }, +}; + +registerBidder(spec); diff --git a/modules/ad2ictionBidAdapter.md b/modules/ad2ictionBidAdapter.md new file mode 100644 index 00000000000..47e355aa795 --- /dev/null +++ b/modules/ad2ictionBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +**Module Name**: Ad2iction Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@ad2iction.com + +# Description + +The Ad2iction Bidding adapter requires setup before beginning. Please contact us on https://www.ad2iction.com. + +# Sample Ad Unit Config +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]] + } + }, + bids: [{ + bidder: 'ad2iction', + params: { + id: 'accepted-uuid' + } + }] + } +]; +``` diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js index 36935e80d3b..d268c4cafa8 100644 --- a/modules/adWMGBidAdapter.js +++ b/modules/adWMGBidAdapter.js @@ -1,9 +1,9 @@ 'use strict'; -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'adWMG'; const ENDPOINT = 'https://hb.adwmg.com/hb'; diff --git a/modules/adagioAnalyticsAdapter.js b/modules/adagioAnalyticsAdapter.js index 96f3f089cbd..de0aa1cb5d7 100644 --- a/modules/adagioAnalyticsAdapter.js +++ b/modules/adagioAnalyticsAdapter.js @@ -5,16 +5,50 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { getWindowTop } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getWindowTop, getWindowSelf, deepAccess, logInfo, logError } from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; const events = Object.keys(CONSTANTS.EVENTS).map(key => CONSTANTS.EVENTS[key]); -const VERSION = '2.0.0'; +const ADAGIO_GVLID = 617; +const VERSION = '3.0.0'; +const PREBID_VERSION = '$prebid.version$'; +const ENDPOINT = 'https://c.4dex.io/pba.gif'; +const cache = { + auctions: {}, + getAuction: function(auctionId, adUnitCode) { + return this.auctions[auctionId][adUnitCode]; + }, + getBiddersFromAuction: function(auctionId, adUnitCode) { + return this.getAuction(auctionId, adUnitCode).bdrs.split(','); + }, + getAllAdUnitCodes: function(auctionId) { + return Object.keys(this.auctions[auctionId]); + }, + updateAuction: function(auctionId, adUnitCode, values) { + this.auctions[auctionId][adUnitCode] = { + ...this.auctions[auctionId][adUnitCode], + ...values + }; + }, -const adagioEnqueue = function adagioEnqueue(action, data) { - getWindowTop().ADAGIO.queue.push({ action, data, ts: Date.now() }); -} + // Map prebid auction id to adagio auction id + auctionIdReferences: {}, + addPrebidAuctionIdRef(auctionId, adagioAuctionId) { + this.auctionIdReferences[auctionId] = adagioAuctionId; + }, + getAdagioAuctionId(auctionId) { + return this.auctionIdReferences[auctionId]; + } +}; +const enc = window.encodeURIComponent; + +/** +/* BEGIN ADAGIO.JS CODE + */ function canAccessTopWindow() { try { @@ -24,12 +58,356 @@ function canAccessTopWindow() { } catch (error) { return false; } +}; + +function getCurrentWindow() { + return currentWindow; +}; + +let currentWindow; + +const adagioEnqueue = function adagioEnqueue(action, data) { + getCurrentWindow().ADAGIO.queue.push({ action, data, ts: Date.now() }); +}; + +/** + * END ADAGIO.JS CODE + */ + +/** + * UTILS FUNCTIONS + */ + +const guard = { + adagio: (value) => isAdagio(value), + bidTracked: (auctionId, adUnitCode) => deepAccess(cache, `auctions.${auctionId}.${adUnitCode}`, false), + auctionTracked: (auctionId) => deepAccess(cache, `auctions.${auctionId}`, false) +}; + +function removeDuplicates(arr, getKey) { + const seen = {}; + return arr.filter(item => { + const key = getKey(item); + return seen.hasOwnProperty(key) ? false : (seen[key] = true); + }); +}; + +function getAdapterNameForAlias(aliasName) { + return adapterManager.aliasRegistry[aliasName] || aliasName; +}; + +function isAdagio(value) { + if (!value) { + return false + } + + return value.toLowerCase().includes('adagio') || + getAdapterNameForAlias(value).toLowerCase().includes('adagio'); +}; + +function getMediaTypeAlias(mediaType) { + const mediaTypesMap = { + banner: 'ban', + outstream: 'vidout', + instream: 'vidin', + adpod: 'vidadpod', + native: 'nat' + }; + return mediaTypesMap[mediaType] || mediaType; +}; + +function addKeyPrefix(obj, prefix) { + return Object.keys(obj).reduce((acc, key) => { + // We don't want to prefix already prefixed keys. + if (key.startsWith(prefix)) { + acc[key] = obj[key]; + return acc; + } + + acc[`${prefix}${key}`] = obj[key]; + return acc; + }, {}); +} + +/** + * sendRequest to Adagio. It filter null values and encode each query param. + * @param {Object} qp + */ +function sendRequest(qp) { + // Removing null values + qp = Object.keys(qp).reduce((acc, key) => { + if (qp[key] !== null) { + acc[key] = qp[key]; + } + return acc; + }, {}); + + const url = `${ENDPOINT}?${Object.keys(qp).map(key => `${key}=${enc(qp[key])}`).join('&')}`; + ajax(url, null, null, {method: 'GET'}); +}; + +/** + * Send a new beacon to Adagio. It increment the version of the beacon. + * @param {string} auctionId + * @param {string} adUnitCode + */ +function sendNewBeacon(auctionId, adUnitCode) { + cache.updateAuction(auctionId, adUnitCode, { + v: (cache.getAuction(auctionId, adUnitCode).v || 0) + 1 + }); + sendRequest(cache.getAuction(auctionId, adUnitCode)); +}; + +function getTargetedAuctionId(bid) { + return deepAccess(bid, 'latestTargetedAuctionId') || deepAccess(bid, 'auctionId'); } +/** + * END UTILS FUNCTIONS + */ + +/** + * HANDLERS + * - handlerAuctionInit + * - handlerBidResponse + * - handlerAuctionEnd + * - handlerBidWon + * - handlerAdRender + * + * Each handler is called when the event is fired. + */ + +function handlerAuctionInit(event) { + const w = getCurrentWindow(); + + const prebidAuctionId = event.auctionId; + const adUnitCodes = removeDuplicates(event.adUnitCodes, adUnitCode => adUnitCode); + + // Check if Adagio is on the bid requests. + // If not, we don't need to track the auction. + const adagioBidRequest = event.bidderRequests.find(bidRequest => isAdagio(bidRequest.bidderCode)); + if (!adagioBidRequest) { + logInfo(`Adagio is not on the bid requests for auction '${prebidAuctionId}'`) + return; + } + + cache.auctions[prebidAuctionId] = {}; + + adUnitCodes.forEach(adUnitCode => { + const adUnits = event.adUnits.filter(adUnit => adUnit.code === adUnitCode); + + // Get all bidders configures for the ad unit. + const bidders = removeDuplicates( + adUnits.map(adUnit => adUnit.bids.map(bid => ({bidder: bid.bidder, params: bid.params}))).flat(), + bidder => bidder.bidder + ); + + // Check if Adagio is configured for the ad unit. + // If not, we don't need to track the ad unit. + const adagioBidder = bidders.find(bidder => isAdagio(bidder.bidder)); + if (!adagioBidder) { + logInfo(`Adagio is not configured for ad unit '${adUnitCode}'`); + return; + } + + // Get all media types and banner sizes configured for the ad unit. + const mediaTypes = adUnits.map(adUnit => adUnit.mediaTypes); + const mediaTypesKeys = removeDuplicates( + mediaTypes.map(mediaTypeObj => Object.keys(mediaTypeObj)).flat(), + mediaTypeKey => mediaTypeKey + ).map(mediaType => getMediaTypeAlias(mediaType)).sort(); + const bannerSizes = removeDuplicates( + mediaTypes.filter(mediaType => mediaType.hasOwnProperty(BANNER)) + .map(mediaType => mediaType[BANNER].sizes.map(size => size.join('x'))) + .flat(), + bannerSize => bannerSize + ).sort(); + + // Get all Adagio bids for the ad unit from the bidRequest. + // If no bids, we don't need to track the ad unit. + const adagioAdUnitBids = adagioBidRequest.bids.filter(bid => bid.adUnitCode === adUnitCode); + if (deepAccess(adagioAdUnitBids, 'length', 0) <= 0) { + logInfo(`Adagio is not on the bid requests for ad unit '${adUnitCode}' and auction '${prebidAuctionId}'`) + return; + } + // Get Adagio params from the first bid. + // We assume that all Adagio bids for a same adunit have the same params. + const params = adagioAdUnitBids[0].params; + + const adagioAuctionId = params.adagioAuctionId; + cache.addPrebidAuctionIdRef(prebidAuctionId, adagioAuctionId); + + // Get all media types requested for Adagio. + const adagioMediaTypes = removeDuplicates( + adagioAdUnitBids.map(bid => Object.keys(bid.mediaTypes)).flat(), + mediaTypeKey => mediaTypeKey + ).flat().map(mediaType => getMediaTypeAlias(mediaType)).sort(); + + const qp = { + v: 0, + pbjsv: PREBID_VERSION, + org_id: params.organizationId, + site: params.site, + pv_id: params.pageviewId, + auct_id: adagioAuctionId, + adu_code: adUnitCode, + url_dmn: w.location.hostname, + pgtyp: params.pagetype, + plcmt: params.placement, + t_n: params.testName || null, + t_v: params.testVersion || null, + mts: mediaTypesKeys.join(','), + ban_szs: bannerSizes.join(','), + bdrs: bidders.map(bidder => getAdapterNameForAlias(bidder.bidder)).sort().join(','), + adg_mts: adagioMediaTypes.join(',') + }; + + cache.auctions[prebidAuctionId][adUnitCode] = qp; + sendNewBeacon(prebidAuctionId, adUnitCode); + }); +}; + +/** + * handlerBidResponse allow to track the adagio bid response + * and to update the auction cache with the seat ID. + * No beacon is sent here. + */ +function handlerBidResponse(event) { + if (!guard.adagio(event.bidder)) { + return; + } + + if (!guard.bidTracked(event.auctionId, event.adUnitCode)) { + return; + } + + if (!event.pba) { + return; + } + + cache.updateAuction(event.auctionId, event.adUnitCode, { + ...addKeyPrefix(event.pba, 'e_') + }); +}; + +function handlerAuctionEnd(event) { + const { auctionId } = event; + + if (!guard.auctionTracked(auctionId)) { + return; + } + + const adUnitCodes = cache.getAllAdUnitCodes(auctionId); + adUnitCodes.forEach(adUnitCode => { + const mapper = (bidder) => event.bidsReceived.find(bid => bid.adUnitCode === adUnitCode && bid.bidder === bidder) ? '1' : '0'; + + cache.updateAuction(auctionId, adUnitCode, { + bdrs_bid: cache.getBiddersFromAuction(auctionId, adUnitCode).map(mapper).join(',') + }); + sendNewBeacon(auctionId, adUnitCode); + }); +} + +function handlerBidWon(event) { + let auctionId = getTargetedAuctionId(event); + + if (!guard.bidTracked(auctionId, event.adUnitCode)) { + return; + } + + let adsCurRateToUSD = (event.currency === 'USD') ? 1 : null; + let ogCurRateToUSD = (event.originalCurrency === 'USD') ? 1 : null; + try { + if (typeof getGlobal().convertCurrency === 'function') { + // Currency module is loaded, we can calculate the conversion rate. + + // Get the conversion rate from the original currency to USD. + ogCurRateToUSD = getGlobal().convertCurrency(1, event.originalCurrency, 'USD'); + // Get the conversion rate from the ad server currency to USD. + adsCurRateToUSD = getGlobal().convertCurrency(1, event.currency, 'USD'); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - handlerBidWon', error); + } + + const adagioAuctionCacheId = ( + (event.latestTargetedAuctionId && event.latestTargetedAuctionId !== event.auctionId) + ? cache.getAdagioAuctionId(event.auctionId) + : null); + + cache.updateAuction(auctionId, event.adUnitCode, { + win_bdr: getAdapterNameForAlias(event.bidder), + win_mt: getMediaTypeAlias(event.mediaType), + win_ban_sz: event.mediaType === BANNER ? `${event.width}x${event.height}` : null, + + // ad server currency + win_cpm: event.cpm, + cur: event.currency, + cur_rate: adsCurRateToUSD, + + // original currency from bidder + og_cpm: event.originalCpm, + og_cur: event.originalCurrency, + og_cur_rate: ogCurRateToUSD, + + // cache bid id + auct_id_c: adagioAuctionCacheId, + }); + sendNewBeacon(auctionId, event.adUnitCode); +}; + +function handlerAdRender(event, isSuccess) { + const { adUnitCode } = event.bid; + let auctionId = getTargetedAuctionId(event.bid); + + if (!guard.bidTracked(auctionId, adUnitCode)) { + return; + } + + cache.updateAuction(auctionId, adUnitCode, { + rndr: isSuccess ? 1 : 0 + }); + sendNewBeacon(auctionId, adUnitCode); +}; + +/** + * END HANDLERS + */ + let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { - track: function({ eventType, args }) { - if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { - adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + track: function(event) { + const { eventType, args } = event; + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + handlerAuctionInit(args); + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + handlerBidResponse(args); + break; + case CONSTANTS.EVENTS.AUCTION_END: + handlerAuctionEnd(args); + break; + case CONSTANTS.EVENTS.BID_WON: + handlerBidWon(args); + break; + // AD_RENDER_SUCCEEDED seems redundant with BID_WON. + // case CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED: + case CONSTANTS.EVENTS.AD_RENDER_FAILED: + handlerAdRender(args, eventType === CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED); + break; + } + } catch (error) { + logError('Error on Adagio Analytics Adapter', error); + } + + try { + if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { + adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - adagio.js', error); } } }); @@ -37,11 +415,8 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { adagioAdapter.originEnableAnalytics = adagioAdapter.enableAnalytics; adagioAdapter.enableAnalytics = config => { - if (!canAccessTopWindow()) { - return; - } - - const w = getWindowTop(); + const w = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf(); + currentWindow = w; w.ADAGIO = w.ADAGIO || {}; w.ADAGIO.queue = w.ADAGIO.queue || []; @@ -53,7 +428,8 @@ adagioAdapter.enableAnalytics = config => { adapterManager.registerAnalyticsAdapter({ adapter: adagioAdapter, - code: 'adagio' + code: 'adagio', + gvlid: ADAGIO_GVLID, }); export default adagioAdapter; diff --git a/modules/adagioAnalyticsAdapter.md b/modules/adagioAnalyticsAdapter.md index 312a26ea8da..9fc2cb0bb88 100644 --- a/modules/adagioAnalyticsAdapter.md +++ b/modules/adagioAnalyticsAdapter.md @@ -8,10 +8,10 @@ Maintainer: dev@adagio.io Analytics adapter for Adagio -# Test Parameters +# Settings -``` -{ - provider: 'adagio' -} +```js + pbjs.enableAnalytics({ + provider: 'adagio', + }); ``` diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index c642dff5a8f..112722383a9 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,12 +1,10 @@ import {find} from '../src/polyfill.js'; import { - _map, cleanObj, deepAccess, deepClone, generateUUID, getDNT, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, getWindowSelf, getWindowTop, @@ -34,6 +32,7 @@ import {OUTSTREAM} from '../src/video.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { userSync } from '../src/userSync.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; @@ -54,8 +53,9 @@ const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0k const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -// This provide a whitelist and a basic validation of OpenRTB 2.6 options used by the Adagio SSP. -// https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf +// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP. +// Accept all options but 'protocol', 'companionad', 'companiontype', 'ext' +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf export const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), 'minduration': (value) => isInteger(value), @@ -569,6 +569,7 @@ function _parseNativeBidResponse(bid) { bid.native = native } +// bidRequest param must be the `bidRequest` object with the original `auctionId` value. function _getFloors(bidRequest) { if (!isFn(bidRequest.getFloor)) { return false; @@ -699,9 +700,9 @@ function getPageDimensions() { } /** -* @todo Move to prebid Core as Utils. -* @returns -*/ + * @todo Move to prebid Core as Utils. + * @returns + */ function getViewPortDimensions() { if (!isSafeFrameWindow() && !canAccessTopWindow()) { return ''; @@ -831,8 +832,8 @@ function getPrintNumber(adUnitCode, bidderRequest) { } /** - * domLoading feature is computed on window.top if reachable. - */ + * domLoading feature is computed on window.top if reachable. + */ function getDomLoadingDuration() { let domLoadingDuration = -1; let performance; @@ -995,7 +996,7 @@ export const spec = { const aucId = generateUUID() - const adUnits = _map(validBidRequests, (rawBidRequest) => { + const adUnits = validBidRequests.map(rawBidRequest => { const bidRequest = deepClone(rawBidRequest); // Fix https://github.com/prebid/Prebid.js/issues/9781 @@ -1019,6 +1020,9 @@ export const spec = { } } + // Enforce the organizationId param to be a string + bidRequest.params.organizationId = bidRequest.params.organizationId.toString(); + // Force the Data Layer key and value to be a String if (bidRequest.params.dataLayer) { if (isStr(bidRequest.params.dataLayer) || isNumber(bidRequest.params.dataLayer) || isArray(bidRequest.params.dataLayer) || isFn(bidRequest.params.dataLayer)) { @@ -1067,7 +1071,10 @@ export const spec = { }); // Handle priceFloors module - const computedFloors = _getFloors(bidRequest); + // We need to use `rawBidRequest` as param because: + // - adagioBidAdapter generates its own auctionId due to transmitTid activity limitation (see https://github.com/prebid/Prebid.js/pull/10079) + // - the priceFloors.getFloor() uses a `_floorDataForAuction` map to store the floors based on the auctionId. + const computedFloors = _getFloors(rawBidRequest); if (isArray(computedFloors) && computedFloors.length) { bidRequest.floors = computedFloors @@ -1111,30 +1118,41 @@ export const spec = { _buildVideoBidRequest(bidRequest); } + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + bidRequest.gpid = gpid; + } + + // store the whole bidRequest (adUnit) object in the ADAGIO namespace. storeRequestInAdagioNS(bidRequest); - // Remove these fields at the very end, so we can still use them before. - delete bidRequest.transactionId; - delete bidRequest.ortb2Imp; - delete bidRequest.ortb2; - delete bidRequest.sizes; + // Remove some params that are not needed on the server side. + delete bidRequest.params.siteId; + + // whitelist the fields that are allowed to be sent to the server. + const adUnit = { + adUnitCode: bidRequest.adUnitCode, + auctionId: bidRequest.auctionId, + bidder: bidRequest.bidder, + bidId: bidRequest.bidId, + params: bidRequest.params, + features: bidRequest.features, + gpid: bidRequest.gpid, + mediaTypes: bidRequest.mediaTypes, + nativeParams: bidRequest.nativeParams, + score: bidRequest.score, + transactionId: bidRequest.transactionId, + } - return bidRequest; + return adUnit; }); // Group ad units by organizationId const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => { - const adUnitCopy = deepClone(adUnit); - adUnitCopy.params.organizationId = adUnitCopy.params.organizationId.toString(); - - // remove useless props - delete adUnitCopy.floorData; - delete adUnitCopy.params.siteId; - delete adUnitCopy.userId; - delete adUnitCopy.userIdAsEids; + const organizationId = adUnit.params.organizationId - groupedAdUnits[adUnitCopy.params.organizationId] = groupedAdUnits[adUnitCopy.params.organizationId] || []; - groupedAdUnits[adUnitCopy.params.organizationId].push(adUnitCopy); + groupedAdUnits[organizationId] = groupedAdUnits[organizationId] || []; + groupedAdUnits[organizationId].push(adUnit); return groupedAdUnits; }, {}); @@ -1148,7 +1166,7 @@ export const spec = { }); // Build one request per organizationId - const requests = _map(Object.keys(groupedAdUnits), organizationId => { + const requests = Object.keys(groupedAdUnits).map(organizationId => { return { method: 'POST', url: ENDPOINT, diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 45f39fc6f2d..19673571982 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -107,10 +107,11 @@ var adUnits = [ cpm: 3.00 // default to 1.00 }, video: { - api: [2, 7], // Required - Your video player must at least support the value 2 and/or 7. + api: [2], // Required - Your video player must at least support the value 2 playbackMethod: [6], // Highly recommended skip: 0 - // OpenRTB video options defined here override ones defined in mediaTypes. + // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. + // Not supported: 'protocol', 'companionad', 'companiontype', 'ext' }, native: { // Optional OpenRTB Native 1.2 request object. Only `context`, `plcmttype` fields are supported. @@ -193,6 +194,8 @@ If the FPD value is an array, the 1st value of this array will be used. placement: 'in_article', adUnitElementId: 'article_outstream', video: { + api: [2], + playbackMethod: [6], skip: 0 }, debug: { diff --git a/modules/adfusionBidAdapter.js b/modules/adfusionBidAdapter.js new file mode 100644 index 00000000000..b3638159c2a --- /dev/null +++ b/modules/adfusionBidAdapter.js @@ -0,0 +1,90 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const adpterVersion = '1.0'; +export const REQUEST_URL = 'https://spicyrtb.com/auction/prebid'; + +export const spec = { + code: 'adfusion', + gvlid: 844, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + isBannerBid, + isVideoBid, +}; + +registerBidder(spec); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + utils.mergeDeep(req, { + at: 1, + ext: { + prebid: { + accountid: bid.params.accountId, + adapterVersion: `${adpterVersion}`, + }, + }, + }); + return req; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids; + }, +}); + +function isBidRequestValid(bidRequest) { + const isValid = bidRequest.params.accountId; + if (!isValid) { + utils.logError('AdFusion adapter bidRequest has no accountId'); + return false; + } + return true; +} + +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter((bid) => isVideoBid(bid)); + let bannerBids = bids.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: REQUEST_URL, + data: converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }), + }; +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner'); +} + +function interpretResponse(resp, req) { + return converter.fromORTB({ request: req.data, response: resp.body }); +} diff --git a/modules/adfusionBidAdapter.md b/modules/adfusionBidAdapter.md new file mode 100644 index 00000000000..803a03ba1a1 --- /dev/null +++ b/modules/adfusionBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: AdFusion Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@adfusion.pl +``` + +# Description + +Module that connects to AdFusion demand sources + +# Banner Test Parameters + +```js +var adUnits = [ + { + code: "test-banner", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + [320, 480], + ], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], + }, +]; +``` + +# Video Test Parameters + +```js +var videoAdUnit = { + code: "video1", + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 480], + mimes: ["video/mp4"], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], +}; +``` diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index bf75756174d..b40378c8e35 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -1,8 +1,10 @@ -import {tryAppendQueryString, getBidIdParameter, escapeUnsafeChars, deepAccess} from '../src/utils.js'; +import {deepAccess, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {escapeUnsafeChars} from '../libraries/htmlEscape/htmlEscape.js'; const ADG_BIDDER_CODE = 'adgeneration'; diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index ac0984fec68..add24772463 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -5,7 +5,6 @@ import { createTrackPixelHtml, deepAccess, deepSetValue, - getAdUnitSizes, getDefinedParams, getDNT, isArray, @@ -22,6 +21,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; /* * In case you're AdKernel whitelable platform's client who needs branded adapter to @@ -29,18 +29,19 @@ import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; * * Please contact prebid@adkernel.com and we'll add your adapter as an alias. */ -const VIDEO_PARAMS = ['pos', 'context', 'placement', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', +const VIDEO_PARAMS = ['pos', 'context', 'placement', 'plcmt', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', 'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed']; const VIDEO_FPD = ['battr', 'pos']; const NATIVE_FPD = ['battr', 'api']; +const BANNER_PARAMS = ['pos']; const BANNER_FPD = ['btype', 'battr', 'pos', 'api']; const VERSION = '1.6'; const SYNC_IFRAME = 1; const SYNC_IMAGE = 2; -const SYNC_TYPES = Object.freeze({ +const SYNC_TYPES = { 1: 'iframe', 2: 'image' -}); +}; const GVLID = 14; const NATIVE_MODEL = [ @@ -66,6 +67,11 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { return acc; }, {}); +const MULTI_FORMAT_SUFFIX = '__mf'; +const MULTI_FORMAT_SUFFIX_BANNER = 'b' + MULTI_FORMAT_SUFFIX; +const MULTI_FORMAT_SUFFIX_VIDEO = 'v' + MULTI_FORMAT_SUFFIX; +const MULTI_FORMAT_SUFFIX_NATIVE = 'n' + MULTI_FORMAT_SUFFIX; + /** * Adapter for requesting bids from AdKernel white-label display platform */ @@ -98,7 +104,6 @@ export const spec = { {code: 'displayioads'}, {code: 'rtbdemand_com'}, {code: 'bidbuddy'}, - {code: 'adliveconnect'}, {code: 'didnadisplay'}, {code: 'qortex'}, {code: 'adpluto'}, @@ -173,6 +178,9 @@ export const spec = { ttl: 360, netRevenue: true }; + if (prBid.requestId.endsWith(MULTI_FORMAT_SUFFIX)) { + prBid.requestId = stripMultiformatSuffix(prBid.requestId); + } if ('banner' in imp) { prBid.mediaType = BANNER; prBid.width = rtbBid.w; @@ -239,13 +247,13 @@ registerBidder(spec); function groupImpressionsByHostZone(bidRequests, refererInfo) { let secure = (refererInfo && refererInfo.page?.indexOf('https:') === 0); return Object.values( - bidRequests.map(bidRequest => buildImp(bidRequest, secure)) + bidRequests.map(bidRequest => buildImps(bidRequest, secure)) .reduce((acc, curr, index) => { let bidRequest = bidRequests[index]; let {zoneId, host} = bidRequest.params; let key = `${host}_${zoneId}`; acc[key] = acc[key] || {host: host, zoneId: zoneId, imps: []}; - acc[key].imps.push(curr); + acc[key].imps.push(...curr); return acc; }, {}) ); @@ -264,57 +272,97 @@ function getBidFloor(bid, mediaType, sizes) { } /** - * Builds rtb imp object for single adunit + * Builds rtb imp object(s) for single adunit * @param bidRequest {BidRequest} * @param secure {boolean} */ -function buildImp(bidRequest, secure) { - const imp = { +function buildImps(bidRequest, secure) { + let imp = { 'id': bidRequest.bidId, 'tagid': bidRequest.adUnitCode }; - var mediaType; + if (secure) { + imp.secure = 1; + } var sizes = []; - - if (deepAccess(bidRequest, 'mediaTypes.banner')) { + let mediaTypes = bidRequest.mediaTypes; + let isMultiformat = (~~!!mediaTypes?.banner + ~~!!mediaTypes?.video + ~~!!mediaTypes?.native) > 1; + let result = []; + let typedImp; + + if (mediaTypes?.banner) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = imp.id + MULTI_FORMAT_SUFFIX_BANNER; + } else { + typedImp = imp; + } sizes = getAdUnitSizes(bidRequest); - imp.banner = { + let pbBanner = mediaTypes.banner; + typedImp.banner = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, BANNER_FPD), + ...getDefinedParamsOrEmpty(pbBanner, BANNER_PARAMS), format: sizes.map(wh => parseGPTSingleSizeArrayToRtbSize(wh)), topframe: 0 }; - populateImpFpd(imp.banner, bidRequest, BANNER_FPD); - mediaType = BANNER; - } else if (deepAccess(bidRequest, 'mediaTypes.video')) { - let video = deepAccess(bidRequest, 'mediaTypes.video'); - imp.video = getDefinedParams(video, VIDEO_PARAMS); - populateImpFpd(imp.video, bidRequest, VIDEO_FPD); - if (video.playerSize) { - sizes = video.playerSize[0]; - imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : BANNER); + result.push(typedImp); + } + + if (mediaTypes?.video) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_VIDEO; + } else { + typedImp = imp; + } + let pbVideo = mediaTypes.video; + typedImp.video = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, VIDEO_FPD), + ...getDefinedParamsOrEmpty(pbVideo, VIDEO_PARAMS) + }; + if (pbVideo.playerSize) { + sizes = pbVideo.playerSize[0]; + typedImp.video = Object.assign(typedImp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); + } else if (pbVideo.w && pbVideo.h) { + typedImp.video.w = pbVideo.w; + typedImp.video.h = pbVideo.h; } - mediaType = VIDEO; - } else if (deepAccess(bidRequest, 'mediaTypes.native')) { - let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); - imp.native = { + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : VIDEO); + result.push(typedImp); + } + + if (mediaTypes?.native) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_NATIVE; + } else { + typedImp = imp; + } + let nativeRequest = buildNativeRequest(mediaTypes.native); + typedImp.native = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, NATIVE_FPD), ver: '1.1', request: JSON.stringify(nativeRequest) }; - populateImpFpd(imp.native, bidRequest, NATIVE_FPD); - mediaType = NATIVE; - } else { - throw new Error('Unsupported bid received'); + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : NATIVE); + result.push(typedImp); } - let floor = getBidFloor(bidRequest, mediaType, sizes); - if (floor) { - imp.bidfloor = floor; + return result; +} + +function initImpBidfloor(imp, bid, sizes, mediaType) { + let bidfloor = getBidFloor(bid, mediaType, sizes); + if (bidfloor) { + imp.bidfloor = bidfloor; } - if (secure) { - imp.secure = 1; +} + +function getDefinedParamsOrEmpty(object, params) { + if (object === undefined) { + return {}; } - return imp; + return getDefinedParams(object, params); } /** @@ -346,19 +394,6 @@ function buildNativeRequest(nativeReq) { return request; } -/** - * Populate impression-level FPD from bid request - * @param target {Object} - * @param bidRequest {BidRequest} - * @param props {String[]} - */ -function populateImpFpd(target, bidRequest, props) { - if (bidRequest.ortb2Imp === undefined) { - return; - } - Object.assign(target, getDefinedParams(bidRequest.ortb2Imp, props)); -} - /** * Builds image asset request */ @@ -645,3 +680,7 @@ function buildNativeAd(nativeResp) { }); return cleanObj(nativeAd); } + +function stripMultiformatSuffix(impid) { + return impid.substr(0, impid.length - MULTI_FORMAT_SUFFIX.length - 1); +} diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index 5db75c656bb..9284d543298 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -14,7 +14,6 @@ import {find} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { deepAccess, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, insertElement, isFn, @@ -28,6 +27,7 @@ import { mergeDeep, parseUrl } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'adlooxAnalyticsAdapter'; diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index c2037429185..727dc84e399 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -25,7 +25,6 @@ import { deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, isArray, isBoolean, isInteger, @@ -37,6 +36,7 @@ import { parseUrl, safeJSONParse } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index 2ee6ecfcb56..b78737722bd 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -4,6 +4,7 @@ import { isFn, deepAccess, logMessage } from '../src/utils.js'; import {config} from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +const GVLID = 149; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; const URL_SYNC = 'https://sync.admanmedia.com'; @@ -57,6 +58,7 @@ function getUserId(eids, id, source, uidExt) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -94,7 +96,9 @@ export const spec = { request.ccpa = bidderRequest.uspConsent; } if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; } if (content) { request.content = content; diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 436f918a0f6..5afe7781d7d 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -1,16 +1,40 @@ -import { getValue, logError, isEmpty, deepAccess, getBidIdParameter, isArray } from '../src/utils.js'; +import {getValue, formatQS, logError, deepAccess, isArray, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +export const OPENRTB = { + NATIVE: { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + SPONSORED: 5, + CTA: 6 + }, + DATA_ASSET_TYPE: { + SPONSORED: 1, + DESC: 2, + CTA_TEXT: 12, + }, + } +}; + let SYNC_URL = ''; const BIDDER_CODE = 'admatic'; + export const spec = { code: BIDDER_CODE, aliases: [ {code: 'pixad'} ], - supportedMediaTypes: [BANNER, VIDEO], - /** f + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + /** + * f * @param {object} bid * @return {boolean} */ @@ -33,23 +57,21 @@ export const spec = { * @return {ServerRequest} */ buildRequests: (validBidRequests, bidderRequest) => { + const tmax = bidderRequest.timeout; const bids = validBidRequests.map(buildRequestObject); - const blacklist = bidderRequest.ortb2; + const ortb = bidderRequest.ortb2; const networkId = getValue(validBidRequests[0].params, 'networkId'); const host = getValue(validBidRequests[0].params, 'host'); const currency = config.getConfig('currency.adServerCurrency') || 'TRY'; const bidderName = validBidRequests[0].bidder; const payload = { - user: { - ua: navigator.userAgent - }, - blacklist: [], + ortb, site: { - page: location.href, - ref: location.origin, + page: bidderRequest.refererInfo.page, + ref: bidderRequest.refererInfo.page, publisher: { - name: location.hostname, + name: bidderRequest.refererInfo.domain, publisherId: networkId } }, @@ -57,17 +79,59 @@ export const spec = { ext: { cur: currency, bidder: bidderName - } + }, + schain: {}, + regs: { + ext: { + } + }, + user: { + ext: {} + }, + at: 1, + tmax: parseInt(tmax) }; - if (!isEmpty(blacklist.badv)) { - payload.blacklist = blacklist.badv; - }; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + const consentStr = (bidderRequest.gdprConsent.consentString) + ? bidderRequest.gdprConsent.consentString.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ''; + const gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + payload.regs.ext.gdpr = gdpr; + payload.regs.ext.consent = consentStr; + } + + if (bidderRequest && bidderRequest.coppa) { + payload.regs.ext.coppa = bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined); + } + + if (bidderRequest && bidderRequest.ortb2?.regs?.gpp) { + payload.regs.ext.gpp = bidderRequest.ortb2?.regs?.gpp; + } + + if (bidderRequest && bidderRequest.ortb2?.regs?.gpp_sid) { + payload.regs.ext.gpp_sid = bidderRequest.ortb2?.regs?.gpp_sid; + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.regs.ext.uspIab = bidderRequest.uspConsent; + } + + if (validBidRequests[0].schain) { + const schain = mapSchain(validBidRequests[0].schain); + if (schain) { + payload.schain = schain; + } + } + + if (validBidRequests[0].userIdAsEids) { + const eids = { eids: validBidRequests[0].userIdAsEids }; + payload.user.ext = { ...payload.user.ext, ...eids }; + } if (payload) { switch (bidderName) { case 'pixad': - SYNC_URL = 'https://static.pixad.com.tr/sync.html'; + SYNC_URL = 'https://static.cdn.pixad.com.tr/sync.html'; break; default: SYNC_URL = 'https://cdn.serve.admatic.com.tr/showad/sync.html'; @@ -78,12 +142,36 @@ export const spec = { } }, - getUserSyncs: function (syncOptions, responses) { - if (syncOptions.iframeEnabled) { - return [{ + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (!hasSynced && syncOptions.iframeEnabled) { + // data is only assigned if params are available to pass to syncEndpoint + let params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = gdprConsent.consentString; + } + } + + if (uspConsent) { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + if (gppConsent?.gppString) { + params['gpp'] = gppConsent.gppString; + params['gpp_sid'] = gppConsent.applicableSections?.toString(); + } + + params = Object.keys(params).length ? `?${formatQS(params)}` : ''; + + hasSynced = true; + return { type: 'iframe', - url: SYNC_URL - }]; + url: SYNC_URL + params + }; } }, @@ -106,6 +194,7 @@ export const spec = { netRevenue: true, creativeId: bid.creative_id, meta: { + model: bid.mime_type, advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, bidder: bid.bidder, @@ -121,6 +210,8 @@ export const spec = { 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) }; bidResponses.push(resbid); @@ -130,6 +221,41 @@ export const spec = { } }; +var hasSynced = false; + +export function resetUserSync() { + hasSynced = false; +} + +/** + * @param {object} schain object set by Publisher + * @returns {object} OpenRTB SupplyChain object + */ +function mapSchain(schain) { + if (!schain) { + return null; + } + if (!validateSchain(schain)) { + logError('AdMatic: required schain params missing'); + return null; + } + return schain; +} + +/** + * @param {object} schain object set by Publisher + * @returns {object} bool + */ +function validateSchain(schain) { + if (!schain.nodes) { + return false; + } + const requiredFields = ['asi', 'sid', 'hp']; + return schain.nodes.every(node => { + return requiredFields.every(field => node[field]); + }); +} + function isUrl(str) { try { URL(str); @@ -156,6 +282,11 @@ function enrichSlotWithFloors(slot, bidRequest) { videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({ size: videoSize, mediaType: VIDEO })); } + if (bidRequest.mediaTypes?.native) { + slotFloors.native = {}; + slotFloors.native['*'] = bidRequest.getFloor({ size: '*', mediaType: NATIVE }); + } + if (Object.keys(slotFloors).length > 0) { if (!slot) { slot = {} @@ -195,6 +326,11 @@ function buildRequestObject(bid) { reqObj.type = 'video'; reqObj.mediatype = bid.mediaTypes.video; } + if (bid.mediaTypes?.native) { + reqObj.type = 'native'; + reqObj.size = [{w: 1, h: 1}]; + reqObj.mediatype = bid.mediaTypes.native; + } if (deepAccess(bid, 'ortb2Imp.ext')) { reqObj.ext = bid.ortb2Imp.ext; @@ -214,10 +350,11 @@ function getSizes(bid) { function concatSizes(bid) { let playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); let videoSizes = deepAccess(bid, 'mediaTypes.video.sizes'); + let nativeSizes = deepAccess(bid, 'mediaTypes.native.sizes'); let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); if (isArray(bannerSizes) || isArray(playerSize) || isArray(videoSizes)) { - let mediaTypesSizes = [bannerSizes, videoSizes, playerSize]; + let mediaTypesSizes = [bannerSizes, videoSizes, nativeSizes, playerSize]; return mediaTypesSizes .reduce(function(acc, currSize) { if (isArray(currSize)) { @@ -232,6 +369,45 @@ function concatSizes(bid) { } } +function interpretNativeAd(adm) { + const native = JSON.parse(adm).native; + const result = { + clickUrl: encodeURI(native.link.url), + impressionTrackers: native.imptrackers + }; + native.assets.forEach(asset => { + switch (asset.id) { + case OPENRTB.NATIVE.ASSET_ID.TITLE: + result.title = asset.title.text; + break; + case OPENRTB.NATIVE.ASSET_ID.IMAGE: + result.image = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.ICON: + result.icon = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.BODY: + result.body = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.SPONSORED: + result.sponsoredBy = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.CTA: + result.cta = asset.data.value; + break; + } + }); + return result; +} + function _validateId(id) { return (parseInt(id) > 0); } diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 1006fef631c..f5f0b5bf665 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -14,6 +14,7 @@ const ALIASES = [ {code: 'futureads', endpoint: 'https://ads.futureads.io/prebid.1.2.aspx'}, {code: 'smn', endpoint: 'https://ads.smn.rs/prebid.1.2.aspx'}, {code: 'admixeradx', endpoint: 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'}, + {code: 'admixerwl', endpoint: 'https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx'}, ]; export const spec = { code: BIDDER_CODE, @@ -23,7 +24,9 @@ export const spec = { * Determines whether or not the given bid request is valid. */ isBidRequestValid: function (bid) { - return !!bid.params.zone; + return bid.bidder === 'admixerwl' + ? !!bid.params.clientId && !!bid.params.endpointId + : !!bid.params.zone; }, /** * Make a server request from the list of BidRequests. @@ -73,12 +76,14 @@ export const spec = { validRequest.forEach((bid) => { let imp = {}; Object.keys(bid).forEach(key => imp[key] = bid[key]); + imp.ortb2 && delete imp.ortb2; payload.imps.push(imp); }); + + let urlForRequest = endpointUrl || getEndpointUrl(bidderRequest.bidderCode) return { method: 'POST', - url: - endpointUrl || getEndpointUrl(bidderRequest.bidderCode), + url: bidderRequest.bidderCode === 'admixerwl' ? `${urlForRequest}?client=${payload.imps[0]?.params?.clientId}` : urlForRequest, data: payload, }; }, diff --git a/modules/admixerBidAdapter.md b/modules/admixerBidAdapter.md index 682f5629115..64f8dd64ee4 100644 --- a/modules/admixerBidAdapter.md +++ b/modules/admixerBidAdapter.md @@ -50,3 +50,48 @@ Please use ```admixer``` as the bidder code. }, ]; ``` + +### AdmixerWL Test Parameters +``` + var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + },{ + code: 'mobile-banner-ad-div', + sizes: [[300, 50]], // a mobile size + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + },{ + code: 'video-ad', + sizes: [[300, 50]], + mediaType: 'video', + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + }, + ]; +``` + diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index e44e2b1471a..a498d056513 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -14,38 +14,150 @@ const ENDPOINT_URL_EUROPE = 'https://europe.delivery.adnuntius.com/i'; const GVLID = 855; const DEFAULT_VAST_VERSION = 'vast4' const MAXIMUM_DEALS_LIMIT = 5; +const VALID_BID_TYPES = ['netBid', 'grossBid']; +const META_DATA_KEY = 'adn.metaData'; -const checkSegment = function (segment) { - if (isStr(segment)) return segment; - if (segment.id) return segment.id -} +export const misc = { + getUnixTimestamp: function (addDays, asMinutes) { + const multiplication = addDays / (asMinutes ? 1440 : 1); + return Date.now() + (addDays && addDays > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0); + } +}; + +const storageTool = (function () { + const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + let metaInternal; + + const getMetaInternal = function () { + if (!storage.localStorageIsEnabled()) { + return []; + } + + let parsedJson; + try { + parsedJson = JSON.parse(storage.getDataFromLocalStorage(META_DATA_KEY)); + } catch (e) { + return []; + } + + let filteredEntries = parsedJson ? parsedJson.filter((datum) => { + if (datum.key === 'voidAuIds' && Array.isArray(datum.value)) { + return true; + } + return datum.key && datum.value && datum.exp && datum.exp > misc.getUnixTimestamp(); + }) : []; + const voidAuIdsEntry = filteredEntries.find(entry => entry.key === 'voidAuIds'); + if (voidAuIdsEntry) { + const now = misc.getUnixTimestamp(); + voidAuIdsEntry.value = voidAuIdsEntry.value.filter(voidAuId => voidAuId.auId && voidAuId.exp > now); + if (!voidAuIdsEntry.value.length) { + filteredEntries = filteredEntries.filter(entry => entry.key !== 'voidAuIds'); + } + } + return filteredEntries; + }; + + const setMetaInternal = function (apiResponse) { + if (!storage.localStorageIsEnabled()) { + return; + } + + const updateVoidAuIds = function (currentVoidAuIds, auIdsAsString) { + const newAuIds = isStr(auIdsAsString) ? auIdsAsString.split(';') : []; + const notNewExistingAuIds = currentVoidAuIds.filter(auIdObj => { + return newAuIds.indexOf(auIdObj.value) < -1; + }) || []; + const oneDayFromNow = misc.getUnixTimestamp(1); + const apiIdsArray = newAuIds.map(auId => { + return { exp: oneDayFromNow, auId: auId }; + }) || []; + return notNewExistingAuIds.concat(apiIdsArray) || []; + } -const getSegmentsFromOrtb = function (ortb2) { - const userData = deepAccess(ortb2, 'user.data'); - let segments = []; - if (userData) { - userData.forEach(userdat => { - if (userdat.segment) { - segments.push(...userdat.segment.filter(checkSegment).map(checkSegment)); + const metaAsObj = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: { value: entry.value, exp: entry.exp } }), {}); + for (const key in apiResponse) { + if (key !== 'voidAuIds') { + metaAsObj[key] = { + value: apiResponse[key], + exp: misc.getUnixTimestamp(100) + } + } + } + const currentAuIds = updateVoidAuIds(metaAsObj.voidAuIds || [], apiResponse.voidAuIds); + if (currentAuIds.length > 0) { + metaAsObj.voidAuIds = { value: currentAuIds }; + } + const metaDataForSaving = Object.entries(metaAsObj).map((entrySet) => { + if (entrySet[0] === 'voidAuIds') { + return { + key: entrySet[0], + value: entrySet[1].value + }; + } + return { + key: entrySet[0], + value: entrySet[1].value, + exp: entrySet[1].exp } }); + storage.setDataInLocalStorage(META_DATA_KEY, JSON.stringify(metaDataForSaving)); + }; + + const getUsi = function (meta, ortb2) { + let usi = (meta && meta.usi) ? meta.usi : false; + if (ortb2 && ortb2.user && ortb2.user.id) { + usi = ortb2.user.id + } + return usi; } - return segments -} -const handleMeta = function () { - const storage = getStorageManager({ bidderCode: BIDDER_CODE }) - let adnMeta = null - if (storage.localStorageIsEnabled()) { - adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) + const getSegmentsFromOrtb = function (ortb2) { + const userData = deepAccess(ortb2, 'user.data'); + let segments = []; + if (userData) { + userData.forEach(userdat => { + if (userdat.segment) { + segments.push(...userdat.segment.map((segment) => { + if (isStr(segment)) return segment; + if (isStr(segment.id)) return segment.id; + }).filter((seg) => !!seg)); + } + }); + } + return segments } - return (adnMeta !== null) ? adnMeta.reduce((acc, cur) => { return { ...acc, [cur.key]: cur.value } }, {}) : {} -} -const getUsi = function (meta, ortb2, bidderRequest) { - let usi = (meta !== null && meta.usi) ? meta.usi : false; - if (ortb2 && ortb2.user && ortb2.user.id) { usi = ortb2.user.id } - return usi + return { + refreshStorage: function (bidderRequest) { + const ortb2 = bidderRequest.ortb2 || {}; + metaInternal = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); + metaInternal.usi = getUsi(metaInternal, ortb2); + if (!metaInternal.usi) { + delete metaInternal.usi; + } + if (metaInternal.voidAuIds) { + metaInternal.voidAuIdsArray = metaInternal.voidAuIds.map((voidAuId) => { + return voidAuId.auId; + }); + } + metaInternal.segments = getSegmentsFromOrtb(ortb2); + }, + saveToStorage: function (serverData) { + setMetaInternal(serverData); + }, + getUrlRelatedData: function () { + const { segments, usi, voidAuIdsArray } = metaInternal; + return { segments, usi, voidAuIdsArray }; + }, + getPayloadRelatedData: function () { + const { segments, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = metaInternal; + return payloadRelatedData; + } + }; +})(); + +const validateBidType = function (bidTypeOption) { + return VALID_BID_TYPES.indexOf(bidTypeOption || '') > -1 ? bidTypeOption : 'bid'; } const AU_ID_REGEX = new RegExp('^[0-9A-Fa-f]{1,20}$'); @@ -62,34 +174,38 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - const networks = {}; - const bidRequests = {}; - const requests = []; - const request = []; - const ortb2 = bidderRequest.ortb2 || {}; - const bidderConfig = config.getConfig(); - - const adnMeta = handleMeta() - const usi = getUsi(adnMeta, ortb2, bidderRequest) - const segments = getSegmentsFromOrtb(ortb2); - const tzo = new Date().getTimezoneOffset(); + const queryParamsAndValues = []; + queryParamsAndValues.push('tzo=' + new Date().getTimezoneOffset()) + queryParamsAndValues.push('format=json') const gdprApplies = deepAccess(bidderRequest, 'gdprConsent.gdprApplies'); const consentString = deepAccess(bidderRequest, 'gdprConsent.consentString'); + if (gdprApplies !== undefined) { + const flag = gdprApplies ? '1' : '0' + queryParamsAndValues.push('consentString=' + consentString); + queryParamsAndValues.push('gdpr=' + flag); + } + + storageTool.refreshStorage(bidderRequest); - request.push('tzo=' + tzo) - request.push('format=json') + const urlRelatedMetaData = storageTool.getUrlRelatedData(); + if (urlRelatedMetaData.segments.length > 0) queryParamsAndValues.push('segments=' + urlRelatedMetaData.segments.join(',')); + if (urlRelatedMetaData.usi) queryParamsAndValues.push('userId=' + urlRelatedMetaData.usi); + + const bidderConfig = config.getConfig(); + if (bidderConfig.useCookie === false) queryParamsAndValues.push('noCookies=true'); + if (bidderConfig.maxDeals > 0) queryParamsAndValues.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); + + const bidRequests = {}; + const networks = {}; - if (gdprApplies !== undefined) request.push('consentString=' + consentString); - if (segments.length > 0) request.push('segments=' + segments.join(',')); - if (usi) request.push('userId=' + usi); - if (bidderConfig.useCookie === false) request.push('noCookies=true'); - if (bidderConfig.maxDeals > 0) request.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); for (let i = 0; i < validBidRequests.length; i++) { - const bid = validBidRequests[i] - let network = bid.params.network || 'network'; - const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); - const targeting = bid.params.targeting || {}; + const bid = validBidRequests[i]; + if ((urlRelatedMetaData.voidAuIdsArray && (urlRelatedMetaData.voidAuIdsArray.indexOf(bid.params.auId) > -1 || urlRelatedMetaData.voidAuIdsArray.indexOf(bid.params.auId.padStart(16, '0')) > -1))) { + // This auId is void. Do NOT waste time and energy sending a request to the server + continue; + } + let network = bid.params.network || 'network'; if (bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context !== 'outstream') { network += '_video' } @@ -100,21 +216,31 @@ export const spec = { networks[network] = networks[network] || {}; networks[network].adUnits = networks[network].adUnits || []; if (bidderRequest && bidderRequest.refererInfo) networks[network].context = bidderRequest.refererInfo.page; - if (adnMeta) networks[network].metaData = adnMeta; - const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.bidId, maxDeals: maxDeals } + + const payloadRelatedData = storageTool.getPayloadRelatedData(); + if (Object.keys(payloadRelatedData).length > 0) { + networks[network].metaData = payloadRelatedData; + } + + const targeting = bid.params.targeting || {}; + const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.params.targetId || bid.bidId }; + const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); + if (maxDeals > 0) { + adUnit.maxDeals = maxDeals; + } if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes networks[network].adUnits.push(adUnit); } + const requests = []; const networkKeys = Object.keys(networks) for (let j = 0; j < networkKeys.length; j++) { const network = networkKeys[j]; - const networkRequest = [...request] - if (network.indexOf('_video') > -1) { networkRequest.push('tt=' + DEFAULT_VAST_VERSION) } + if (network.indexOf('_video') > -1) { queryParamsAndValues.push('tt=' + DEFAULT_VAST_VERSION) } const requestURL = gdprApplies ? ENDPOINT_URL_EUROPE : ENDPOINT_URL requests.push({ method: 'POST', - url: requestURL + '?' + networkRequest.join('&'), + url: requestURL + '?' + queryParamsAndValues.join('&'), data: JSON.stringify(networks[network]), bid: bidRequests[network] }); @@ -124,8 +250,20 @@ export const spec = { }, interpretResponse: function (serverResponse, bidRequest) { + if (serverResponse.body.metaData) { + storageTool.saveToStorage(serverResponse.body.metaData); + } const adUnits = serverResponse.body.adUnits; + let validatedBidType = validateBidType(config.getConfig().bidType); + if (bidRequest.bid) { + bidRequest.bid.forEach(b => { + if (b.params && b.params.bidType) { + validatedBidType = validateBidType(b.params.bidType); + } + }); + } + function buildAdResponse(bidderCode, ad, adUnit, dealCount) { const destinationUrls = ad.destinationUrls || {}; const advertiserDomains = []; @@ -135,7 +273,7 @@ export const spec = { const adResponse = { bidderCode: bidderCode, requestId: adUnit.targetId, - cpm: (ad.bid) ? ad.bid.amount * 1000 : 0, + cpm: ad[validatedBidType] ? ad[validatedBidType].amount * 1000 : 0, width: Number(ad.creativeWidth), height: Number(ad.creativeHeight), creativeId: ad.creativeId, diff --git a/modules/adpod.js b/modules/adpod.js index 4ab8e4e5ab9..f6d8309cd9f 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -13,7 +13,6 @@ */ import { - compareOn, deepAccess, generateUUID, groupBy, @@ -28,7 +27,6 @@ import { import { addBidToAuction, AUCTION_IN_PROGRESS, - doCallbacksIfTimedout, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; @@ -212,9 +210,6 @@ function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) { store(bidList, function (error, cacheIds) { if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`); - for (let i = 0; i < bidList.length; i++) { - doCallbacksIfTimedout(auctionInstance, bidList[i]); - } } else { for (let i = 0; i < cacheIds.length; i++) { // when uuid in response is empty string then the key already existed, so this bid wasn't cached @@ -324,7 +319,7 @@ export function checkAdUnitSetupHook(fn, adUnits) { * @param {Object} videoMediaType 'mediaTypes.video' associated to bidResponse * @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory * @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine -*/ + */ function checkBidDuration(videoMediaType, bidResponse) { const buffer = 2; let bidDuration = deepAccess(bidResponse, 'video.durationSeconds'); @@ -595,6 +590,23 @@ function getAdPodAdUnits(codes) { .filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true); } +/** + * This function will create compare function to sort on object property + * @param {string} property + * @returns {function} compare function to be used in sorting + */ +function compareOn(property) { + return function compare(a, b) { + if (a[property] < b[property]) { + return 1; + } + if (a[property] > b[property]) { + return -1; + } + return 0; + } +} + /** * This function removes bids of same category. It will be used when competitive exclusion is enabled. * @param {Array[Object]} bidsReceived diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index 0f445fdfd78..6b0d642468b 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -17,7 +17,8 @@ export const spec = { gvlid: ADQUERY_GVLID, supportedMediaTypes: [BANNER], - /** f + /** + * f * @param {object} bid * @return {boolean} */ diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index f1f92e5dd5e..3c9c661b09c 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -1,8 +1,5 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, @@ -21,7 +18,10 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 1af0cffa700..5bce315f572 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,5 +1,5 @@ // ADRIVER BID ADAPTER for Prebid 1.13 -import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; +import {logInfo, getWindowLocation, _each, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/adstirBidAdapter.js b/modules/adstirBidAdapter.js new file mode 100644 index 00000000000..4b22d568785 --- /dev/null +++ b/modules/adstirBidAdapter.js @@ -0,0 +1,91 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adstir'; +const ENDPOINT = 'https://ad.ad-stir.com/prebid' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(utils.isStr(bid.params.appId) && !utils.isEmptyStr(bid.params.appId) && utils.isInteger(bid.params.adSpaceNo)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const sua = utils.deepAccess(validBidRequests[0], 'ortb2.device.sua', null); + + const requests = validBidRequests.map((r) => { + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify({ + appId: r.params.appId, + adSpaceNo: r.params.adSpaceNo, + auctionId: r.auctionId, + transactionId: r.transactionId, + bidId: r.bidId, + mediaTypes: r.mediaTypes, + sizes: r.sizes, + ref: { + page: bidderRequest.refererInfo.page, + tloc: bidderRequest.refererInfo.topmostLocation, + referrer: bidderRequest.refererInfo.ref, + topurl: config.getConfig('pageUrl') ? false : bidderRequest.refererInfo.reachedTop, + }, + sua, + gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies', false), + usp: (bidderRequest.uspConsent || '1---') !== '1---', + eids: utils.deepAccess(r, 'userIdAsEids', []), + schain: serializeSchain(utils.deepAccess(r, 'schain', null)), + pbVersion: '$prebid.version$', + }), + } + }); + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const seatbid = serverResponse.body.seatbid; + if (!utils.isArray(seatbid)) { + return []; + } + const bids = []; + seatbid.forEach((b) => { + const bid = b.bid || null; + if (!bid) { + return; + } + bids.push(bid); + }); + return bids; + }, +} + +function serializeSchain(schain) { + if (!schain) { + return null; + } + + let serializedSchain = `${schain.ver},${schain.complete}`; + + schain.nodes.map(node => { + serializedSchain += `!${encodeURIComponentForRFC3986(node.asi || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.sid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.hp || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.rid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.name || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.domain || '')}`; + }); + + return serializedSchain; +} + +function encodeURIComponentForRFC3986(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16)}`); +} + +registerBidder(spec); diff --git a/modules/adstirBidAdapter.md b/modules/adstirBidAdapter.md new file mode 100644 index 00000000000..7485375a09d --- /dev/null +++ b/modules/adstirBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: adstir Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@ad-stir.com +``` + +# Description + +Module that connects to adstir's demand sources + +# Test Parameters + +``` + var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adstir', + params: { + appId: 'TEST-MEDIA', + adSpaceNo: 1, + } + } + ] + } + ]; +``` diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index 89ba4878acf..a1dec5a420f 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -1,8 +1,9 @@ -import {_map, chunk, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const ENDPOINT = 'https://ghb.console.adtarget.com.tr/v2/auction/'; const BIDDER_CODE = 'adtarget'; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index cab2b8956bc..8e6aeecdd75 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -1,9 +1,11 @@ -import {_map, chunk, convertTypes, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const subdomainSuffixes = ['', 1, 2]; const AUCTION_PATH = '/v2/auction/'; @@ -15,17 +17,12 @@ const HOST_GETTERS = { return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; } }()), - navelix: () => 'ghb.hb.navelix.com', - appaloosa: () => 'ghb.hb.appaloosa.media', - onefiftytwomedia: () => 'ghb.ads.152media.com', - bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', janet: () => 'ghb.bidder.jmgads.com', - pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', - vidcrunchllc: () => 'ghb.platform.vidcrunch.com', '9dotsmedia': () => 'ghb.platform.audiodots.com', - copper6: () => 'ghb.app.copper6.com' + copper6: () => 'ghb.app.copper6.com', + indicue: () => 'ghb.console.indicue.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -42,18 +39,13 @@ export const spec = { code: BIDDER_CODE, gvlid: 410, aliases: [ - 'onefiftytwomedia', - 'appaloosa', - 'bidsxchange', 'streamkey', 'janet', { code: 'selectmedia', gvlid: 775 }, - { code: 'navelix', gvlid: 380 }, - 'pgam', { code: 'ocm', gvlid: 1148 }, - { code: 'vidcrunchllc', gvlid: 1145 }, '9dotsmedia', 'copper6', + 'indicue', ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 1ea5f1a0096..49187da2fe2 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -1,7 +1,8 @@ -import {deepClone, getAdUnitSizes, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; +import {deepClone, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; export const BIDDER_CODE = 'aduptech'; export const GVLID = 647; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 5930f3adb67..dda88575ff5 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,307 +1,65 @@ // jshint esversion: 6, es3: false, node: true -'use strict'; - -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { convertTypes } from '../libraries/transformParamsUtils/convertTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { - _map, - deepAccess, - deepSetValue, - getDNT, isArray, - isPlainObject, - isStr, - mergeDeep, - parseSizesInput, replaceAuctionPrice, - triggerPixel + triggerPixel, + logMessage, + deepSetValue, + getBidIdParameter } from '../src/utils.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - -const { getConfig } = config; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adxcg'; const SECURE_BID_URL = 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; -const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' - }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 - }, - body: { - id: 4, - name: 'data', - type: 2 - }, - cta: { - id: 1, - type: 12, - name: 'data' - } -}; +const DEFAULT_CURRENCY = 'EUR'; +const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; +const DEFAULT_TMAX = 500; +/** + * Adxcg Bid Adapter. + * + */ export const spec = { + code: BIDDER_CODE, - supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], + + aliases: ['mediaopti'], + + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid: (bid) => { + logMessage('adxcg - validating isBidRequestValid'); const params = bid.params || {}; const { adzoneid } = params; return !!(adzoneid); }, - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let app, site; - - const commonFpd = bidderRequest.ortb2 || {}; - let { user } = commonFpd; - - if (typeof getConfig('app') === 'object') { - app = getConfig('app') || {}; - if (commonFpd.app) { - mergeDeep(app, commonFpd.app); - } - } else { - site = getConfig('site') || {}; - if (commonFpd.site) { - mergeDeep(site, commonFpd.site); - } - - if (!site.page) { - site.page = bidderRequest.refererInfo.page; - site.domain = bidderRequest.refererInfo.domain; - } - } - - const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; - device.ua = device.ua || navigator.userAgent; - device.dnt = getDNT() ? 1 : 0; - device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - - const tid = bidderRequest.ortb2?.source?.tid; - const test = setOnAny(validBidRequests, 'params.test'); - const currency = getConfig('currency.adServerCurrency'); - const cur = currency && [ currency ]; - const eids = setOnAny(validBidRequests, 'userIdAsEids'); - const schain = setOnAny(validBidRequests, 'schain'); - - const imp = validBidRequests.map((bid, id) => { - const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' - }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; - const { adzoneid } = bid.params; - - const imp = { - id: id + 1, - tagid: adzoneid, - secure: 1, - bidfloor, - bidfloorcur, - ext: { - } - }; - - const assets = _map(bid.nativeParams, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - const asset = { - required: bidParams.required & 1, - }; - if (props) { - asset.id = props.id; - let wmin, hmin, w, h; - let aRatios = bidParams.aspect_ratios; - - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - } - - if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); - w = sizes[0]; - h = sizes[1]; - } - - asset[props.name] = { - len: bidParams.len, - type: props.type, - wmin, - hmin, - w, - h - }; - - return asset; - } - }).filter(Boolean); - - if (assets.length) { - imp.native = { - request: JSON.stringify({assets: assets}) - }; - } - - const bannerParams = deepAccess(bid, 'mediaTypes.banner'); - - if (bannerParams && bannerParams.sizes) { - const sizes = parseSizesInput(bannerParams.sizes); - const format = sizes.map(size => { - const [ width, height ] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); - - imp.banner = { - format - }; - } - - const videoParams = deepAccess(bid, 'mediaTypes.video'); - if (videoParams) { - imp.video = videoParams; - } - - return imp; - }); - - const request = { - id: bidderRequest.auctionId, - site, - app, - user, - geo: { utcoffset: new Date().getTimezoneOffset() }, - device, - source: { tid, fd: 1 }, - ext: { - prebid: { - channel: { - name: 'pbjs', - version: '$prebid.version$' - } - } - }, - cur, - imp - }; - - if (test) { - request.is_debug = !!test; - request.test = 1; - } - if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); - } - - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (eids) { - deepSetValue(request, 'user.ext.eids', eids); - } - - if (schain) { - deepSetValue(request, 'source.ext.schain', schain); - } + buildRequests: (bidRequests, bidderRequest) => { + const data = converter.toORTB({ bidRequests, bidderRequest }); return { method: 'POST', url: SECURE_BID_URL, - data: JSON.stringify(request), + data, options: { contentType: 'application/json' }, - bids: validBidRequests + bidderRequest }; }, - interpretResponse: function(serverResponse, { bids }) { - if (!serverResponse.body) { - return; - } - const { seatbid, cur } = serverResponse.body; - - const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { - result[bid.impid - 1] = bid; - return result; - }, []); - - return bids.map((bid, id) => { - const bidResponse = bidResponses[id]; - if (bidResponse) { - const mediaType = deepAccess(bidResponse, 'ext.crType'); - const result = { - requestId: bid.bidId, - cpm: bidResponse.price, - creativeId: bidResponse.crid, - ttl: bidResponse.ttl ? bidResponse.ttl : 300, - netRevenue: bid.netRevenue === 'net', - currency: cur, - burl: bid.burl || '', - mediaType: mediaType, - width: bidResponse.w, - height: bidResponse.h, - dealId: bidResponse.dealid, - }; - deepSetValue(result, 'meta.mediaType', mediaType); - if (isArray(bidResponse.adomain)) { - deepSetValue(result, 'meta.advertiserDomains', bidResponse.adomain); - } - - if (isPlainObject(bidResponse.ext)) { - if (isStr(bidResponse.ext.mediaType)) { - deepSetValue(result, 'meta.mediaType', mediaType); - } - if (isStr(bidResponse.ext.advertiser_id)) { - deepSetValue(result, 'meta.advertiserId', bidResponse.ext.advertiser_id); - } - if (isStr(bidResponse.ext.advertiser_name)) { - deepSetValue(result, 'meta.advertiserName', bidResponse.ext.advertiser_name); - } - if (isStr(bidResponse.ext.agency_name)) { - deepSetValue(result, 'meta.agencyName', bidResponse.ext.agency_name); - } - } - if (mediaType === BANNER) { - result.ad = bidResponse.adm; - } else if (mediaType === NATIVE) { - result.native = parseNative(bidResponse); - result.width = 0; - result.height = 0; - } else if (mediaType === VIDEO) { - result.vastUrl = bidResponse.nurl; - result.vastXml = bidResponse.adm; - } - - return result; - } - }).filter(Boolean); + interpretResponse: (response, request) => { + if (response.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { const syncs = []; let syncUrl = config.getConfig('adxcg.usersyncUrl'); @@ -323,44 +81,95 @@ export const spec = { } return syncs; }, + onBidWon: (bid) => { // for native requests we put the nurl as an imp tracker, otherwise if the auction takes place on prebid server // the server JS adapter puts the nurl in the adm as a tracking pixel and removes the attribute if (bid.nurl) { triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm)) } + }, + transformBidParams: function (params) { + return convertTypes({ + 'cf': 'string', + 'cp': 'number', + 'ct': 'number', + 'adzoneid': 'string' + }, params); } }; -registerBidder(spec); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: 'EUR' + }, -function parseNative(bid) { - const { assets, link, imptrackers, jstracker } = JSON.parse(bid.adm); - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || undefined, - impressionTrackers: imptrackers || undefined, - javascriptTrackers: jstracker ? [ jstracker ] : undefined - }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // tagid + imp.tagid = bidRequest.params.adzoneid.toString(); + // unknown params + const unknownParams = slotUnknownParams(bidRequest); + if (imp.ext || unknownParams) { + imp.ext = Object.assign({}, imp.ext, unknownParams); + } + // battr + if (bidRequest.params.battr) { + ['banner', 'video', 'audio', 'native'].forEach(k => { + if (imp[k]) { + imp[k].battr = bidRequest.params.battr; + } + }); + } + // deals + if (bidRequest.params.deals && isArray(bidRequest.params.deals)) { + imp.pmp = { + private_auction: 0, + deals: bidRequest.params.deals + }; } - }); - return result; -} -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; + imp.secure = Number(window.location.protocol === 'https:'); + + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } - } -} + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + request.tmax = request.tmax || DEFAULT_TMAX; + request.test = config.getConfig('debug') ? 1 : 0; + request.at = 1; + deepSetValue(request, 'ext.prebid.channel.name', 'pbjs'); + deepSetValue(request, 'ext.prebid.channel.version', '$prebid.version$'); + return request; + }, -function flatten(arr) { - return [].concat(...arr); + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; + }, +}); + +/** + * Unknown params are captured and sent on ext + */ +function slotUnknownParams(slot) { + const ext = {}; + const knownParamsMap = {}; + KNOWN_PARAMS.forEach(value => knownParamsMap[value] = 1); + Object.keys(slot.params).forEach(key => { + if (!knownParamsMap[key]) { + ext[key] = slot.params[key]; + } + }); + return Object.keys(ext).length > 0 ? { prebid: ext } : null; } + +registerBidder(spec); diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 6eb897d334e..8952c3ae2b9 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,5 +1,6 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; @@ -166,6 +167,50 @@ export const spec = { } }); return bidResponses; + }, + + /** + * List user sync endpoints. + * Legal information have to be added to the request. + * Only iframe syncs are supported. + * + * @param {*} syncOptions Publisher prebid configuration. + * @param {*} serverResponses A successful response from the server. + * @return {syncs[]} An array of syncs that should be executed. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled) { + return []; + } + + let params = ''; + + // GDPR + if (gdprConsent) { + params += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + params += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + params += '&coppa=1'; + } + + // CCPA + if (uspConsent) { + params += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // GPP + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppConsent.gppString); + params += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + + return [{ + type: 'iframe', + url: `https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4${params}` + }]; } } diff --git a/modules/agmaAnalyticsAdapter.js b/modules/agmaAnalyticsAdapter.js new file mode 100644 index 00000000000..afbc3e771ec --- /dev/null +++ b/modules/agmaAnalyticsAdapter.js @@ -0,0 +1,225 @@ +import { ajax } from '../src/ajax.js'; +import { + generateUUID, + logInfo, + logError, + getPerformanceNow, + isEmpty, + isEmptyStr, +} from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { config } from '../src/config.js'; + +const GVLID = 1122; +const ModuleCode = 'agma'; +const analyticsType = 'endpoint'; +const scriptVersion = '1.7.0'; +const batchDelayInMs = 1000; +const agmaURL = 'https://pbc.agma-analytics.de/v1'; +const pageViewId = generateUUID(); + +const { + EVENTS: { AUCTION_INIT }, +} = CONSTANTS; + +// Helper functions +const getScreen = () => { + const w = window; + const d = document; + const e = d.documentElement; + const g = d.getElementsByTagName('body')[0]; + const x = w.innerWidth || e.clientWidth || g.clientWidth; + const y = w.innerHeight || e.clientHeight || g.clientHeight; + return { x, y }; +}; + +const getUserIDs = () => { + try { + return getGlobal().getUserIdsAsEids(); + } catch (e) {} + return []; +}; + +export const getOrtb2Data = (options) => { + let site = null; + let user = null; + + // check if data is provided via config + if (options.ortb2) { + if (options.ortb2.user) { + user = options.ortb2.user; + } + if (options.ortb2.site) { + site = options.ortb2.site; + } + if (site && user) { + return { site, user }; + } + } + try { + const configData = config.getConfig('agma'); + // try to fallback to global config + if (configData.ortb2) { + site = site || configData.ortb2.site; + user = user || configData.ortb2.user; + } + } catch (e) {} + + return { site, user }; +}; + +export const getTiming = () => { + // Timing API V2 + let ttfb = 0; + try { + const entry = performance.getEntriesByType('navigation')[0]; + ttfb = Math.round(entry.responseStart - entry.startTime); + } catch (e) { + // Timing API V1 + try { + const entry = performance.timing; + ttfb = Math.round(entry.responseStart - entry.fetchStart); + } catch (e) { + // Timing API not available + return null; + } + } + const elapsedTime = getPerformanceNow(); + ttfb = ttfb >= 0 && ttfb <= elapsedTime ? ttfb : 0; + return { + ttfb, + elapsedTime, + }; +}; + +export const getPayload = (auctionIds, options) => { + if (!options || !auctionIds || auctionIds.length === 0) { + return false; + } + const consentData = gdprDataHandler.getConsentData(); + let gdprApplies = true; // we assume gdpr applies + let useExtendedPayload = false; + if (consentData) { + gdprApplies = consentData.gdprApplies; + const consents = consentData.vendorData?.vendor?.consents || {}; + useExtendedPayload = consents[GVLID]; + } + const ortb2 = getOrtb2Data(options); + const ri = getRefererInfo() || {}; + + let payload = { + auctionIds: auctionIds, + triggerEvent: options.triggerEvent, + pageViewId, + domain: ri.domain, + gdprApplies, + code: options.code, + ortb2: { site: ortb2.site }, + pageUrl: ri.page, + prebidVersion: '$prebid.version$', + scriptVersion, + debug: options.debug, + timing: getTiming(), + }; + + if (useExtendedPayload) { + const { x, y } = getScreen(); + const userIdsAsEids = getUserIDs(); + payload = { + ...payload, + ortb2, + extended: true, + timestamp: Date.now(), + gdprConsentString: consentData.consentString, + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + referrer: ri.topmostLocation, + pageUrl: ri.page, + screenWidth: x, + screenHeight: y, + userIdsAsEids, + }; + } + return payload; +}; + +const agmaAnalytics = Object.assign(adapter({ analyticsType }), { + auctionIds: [], + timer: null, + track(data) { + const { eventType, args } = data; + if (eventType === this.options.triggerEvent && args && args.auctionId) { + this.auctionIds.push(args.auctionId); + if (this.timer === null) { + this.timer = setTimeout(() => { + this.processBatch(); + }, batchDelayInMs); + } + } + }, + processBatch() { + const currentBatch = [...this.auctionIds]; + const payload = getPayload(currentBatch, this.options); + this.auctionIds = []; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.send(payload); + }, + send(payload) { + if (!payload) { + return; + } + return ajax( + agmaURL, + () => { + logInfo(ModuleCode, 'flushed', payload); + }, + JSON.stringify(payload), + { + contentType: 'text/plain', + method: 'POST', + } + ); + }, +}); + +agmaAnalytics.originEnableAnalytics = agmaAnalytics.enableAnalytics; +agmaAnalytics.enableAnalytics = function (config = {}) { + const { options } = config; + + if (isEmpty(options)) { + logError(ModuleCode, 'Please set options'); + return false; + } + + if (options.site && !options.code) { + logError(ModuleCode, 'Please set `code` - `site` is deprecated'); + options.code = options.site; + } + + if (!options.code || isEmptyStr(options.code)) { + logError(ModuleCode, 'Please set `code` option - agma Analytics is disabled'); + return false; + } + + agmaAnalytics.options = { + triggerEvent: AUCTION_INIT, + ...options, + }; + + agmaAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: agmaAnalytics, + code: ModuleCode, + gvlid: GVLID, +}); + +export default agmaAnalytics; diff --git a/modules/agmaAnalyticsAdapter.md b/modules/agmaAnalyticsAdapter.md new file mode 100644 index 00000000000..30c88fb92ec --- /dev/null +++ b/modules/agmaAnalyticsAdapter.md @@ -0,0 +1,28 @@ +# Overview + Module Name: Agma Analytics + Module Type: Analytics Adapter + Maintainer: [www.agma-mmc.de](https://www.agma-mmc.de) + Technical Support: [info@mllrsohn.com](mailto:info@mllrsohn.com) + +# Description + +Agma Analytics adapter. Please contact [team-internet@agma-mmc.de](mailto:team-internet@agma-mmc.de) for signup and access to [futher documentation](https://docs.agma-analytics.de). + +# Usage + +Add the `agmaAnalyticsAdapter` to your build: + +``` +gulp build --modules=...,agmaAnalyticsAdapter... +``` + +Configure the analytics module: + +```javascript +pbjs.enableAnalytics({ + provider: 'agma', + options: { + code: 'provided-by-agma' // change to the code you received from agma + } +}); +``` diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index ffab41611ef..9e2d4efb5ff 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -1,7 +1,7 @@ -import { getBidIdParameter, tryAppendQueryString, createTrackPixelHtml, logError, logWarn, deepAccess } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; +import {createTrackPixelHtml, logError, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER, NATIVE } from '../src/mediaTypes.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BidderCode = 'aja'; const URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; @@ -24,7 +24,7 @@ const BannerSizeMap = { export const spec = { code: BidderCode, - supportedMediaTypes: [VIDEO, BANNER, NATIVE], + supportedMediaTypes: [BANNER], /** * Determines whether or not the given bid has all the params needed to make a valid request. @@ -50,12 +50,20 @@ export const spec = { for (let i = 0, len = validBidRequests.length; i < len; i++) { const bidRequest = validBidRequests[i]; + if ( + (bidRequest.mediaTypes?.native || bidRequest.mediaTypes?.video) && + bidRequest.mediaTypes?.banner) { + continue + } + let queryString = ''; const asi = getBidIdParameter('asi', bidRequest.params); queryString = tryAppendQueryString(queryString, 'asi', asi); queryString = tryAppendQueryString(queryString, 'skt', SDKType); + queryString = tryAppendQueryString(queryString, 'gpid', bidRequest.ortb2Imp?.ext?.gpid) queryString = tryAppendQueryString(queryString, 'tid', bidRequest.ortb2Imp?.ext?.tid) + queryString = tryAppendQueryString(queryString, 'cdep', bidRequest.ortb2?.device?.ext?.cdep) queryString = tryAppendQueryString(queryString, 'prebid_id', bidRequest.bidId); queryString = tryAppendQueryString(queryString, 'prebid_ver', '$prebid.version$'); @@ -63,19 +71,8 @@ export const spec = { queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); } - const banner = deepAccess(bidRequest, `mediaTypes.${BANNER}`) - if (banner) { - const adFormatIDs = []; - for (const size of banner.sizes || []) { - if (size.length !== 2) { - continue - } - - const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; - if (adFormatID) { - adFormatIDs.push(adFormatID); - } - } + const adFormatIDs = pickAdFormats(bidRequest) + if (adFormatIDs && adFormatIDs.length > 0) { queryString = tryAppendQueryString(queryString, 'ad_format_ids', adFormatIDs.join(',')); } @@ -86,7 +83,7 @@ export const spec = { })); } - const sua = deepAccess(bidRequest, 'ortb2.device.sua'); + const sua = bidRequest.ortb2?.device?.sua if (sua) { queryString = tryAppendQueryString(queryString, 'sua', JSON.stringify(sua)); } @@ -109,9 +106,17 @@ export const spec = { } const ad = bidderResponseBody.ad; + if (AdType.Banner !== ad.ad_type) { + return [] + } + const bannerAd = bidderResponseBody.ad.banner; const bid = { requestId: ad.prebid_id, + mediaType: BANNER, + ad: bannerAd.tag, + width: bannerAd.w, + height: bannerAd.h, cpm: ad.price, creativeId: ad.creative_id, dealId: ad.deal_id, @@ -119,80 +124,16 @@ export const spec = { netRevenue: true, ttl: 300, // 5 minutes meta: { - advertiserDomains: [] + advertiserDomains: bannerAd.adomain, }, } - - if (AdType.Video === ad.ad_type) { - const videoAd = bidderResponseBody.ad.video; - Object.assign(bid, { - vastXml: videoAd.vtag, - width: videoAd.w, - height: videoAd.h, - renderer: newRenderer(bidderResponseBody), - adResponse: bidderResponseBody, - mediaType: VIDEO - }); - - Array.prototype.push.apply(bid.meta.advertiserDomains, videoAd.adomain) - } else if (AdType.Banner === ad.ad_type) { - const bannerAd = bidderResponseBody.ad.banner; - Object.assign(bid, { - width: bannerAd.w, - height: bannerAd.h, - ad: bannerAd.tag, - mediaType: BANNER + try { + bannerAd.imps.forEach(impTracker => { + const tracker = createTrackPixelHtml(impTracker); + bid.ad += tracker; }); - try { - bannerAd.imps.forEach(impTracker => { - const tracker = createTrackPixelHtml(impTracker); - bid.ad += tracker; - }); - } catch (error) { - logError('Error appending tracking pixel', error); - } - - Array.prototype.push.apply(bid.meta.advertiserDomains, bannerAd.adomain) - } else if (AdType.Native === ad.ad_type) { - const nativeAds = ad.native.template_and_ads.ads; - if (nativeAds.length === 0) { - return []; - } - - const nativeAd = nativeAds[0]; - const assets = nativeAd.assets; - - Object.assign(bid, { - mediaType: NATIVE - }); - - bid.native = { - title: assets.title, - body: assets.description, - cta: assets.cta_text, - sponsoredBy: assets.sponsor, - clickUrl: assets.lp_link, - impressionTrackers: nativeAd.imps, - privacyLink: assets.adchoice_url - }; - - if (assets.img_main !== undefined) { - bid.native.image = { - url: assets.img_main, - width: parseInt(assets.img_main_width, 10), - height: parseInt(assets.img_main_height, 10) - }; - } - - if (assets.img_icon !== undefined) { - bid.native.icon = { - url: assets.img_icon, - width: parseInt(assets.img_icon_width, 10), - height: parseInt(assets.img_icon_height, 10) - }; - } - - Array.prototype.push.apply(bid.meta.advertiserDomains, nativeAd.adomain) + } catch (error) { + logError('Error appending tracking pixel', error); } return [bid]; @@ -228,34 +169,23 @@ export const spec = { }, } -function newRenderer(bidderResponse) { - const renderer = Renderer.install({ - id: bidderResponse.ad.prebid_id, - url: bidderResponse.ad.video.purl, - loaded: false, - }); +function pickAdFormats(bidRequest) { + let sizes = bidRequest.sizes || [] + sizes.push(...(bidRequest.mediaTypes?.banner?.sizes || [])) - try { - renderer.setRender(outstreamRender); - } catch (err) { - logWarn('Prebid Error calling setRender on newRenderer', err); - } + const adFormatIDs = []; + for (const size of sizes) { + if (size.length !== 2) { + continue + } - return renderer; -} + const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; + if (adFormatID) { + adFormatIDs.push(adFormatID); + } + } -function outstreamRender(bid) { - bid.renderer.push(() => { - window['aja_vast_player'].init({ - vast_tag: bid.adResponse.ad.video.vtag, - ad_unit_code: bid.adUnitCode, // target div id to render video - width: bid.width, - height: bid.height, - progress: bid.adResponse.ad.video.progress, - loop: bid.adResponse.ad.video.loop, - inread: bid.adResponse.ad.video.inread - }); - }); + return [...new Set(adFormatIDs)] } registerBidder(spec); diff --git a/modules/ajaBidAdapter.md b/modules/ajaBidAdapter.md index 66155875f4d..92ffecaeb9f 100644 --- a/modules/ajaBidAdapter.md +++ b/modules/ajaBidAdapter.md @@ -8,7 +8,7 @@ Maintainer: ssp_support@aja-kk.co.jp # Description Connects to Aja exchange for bids. -Aja bid adapter supports Banner and Outstream Video. +Aja bid adapter supports Banner. # Test Parameters ```js @@ -29,64 +29,6 @@ var adUnits = [ asi: 'tk82gbLmg' } }] - }, - // Video outstream adUnit - { - code: 'prebid_video', - mediaTypes: { - video: { - context: 'outstream', - playerSize: [300, 250] - } - }, - bids: [{ - bidder: 'aja', - params: { - asi: '1-KwEG_iR' - } - }] - }, - // Native adUnit - { - code: 'prebid_native', - mediaTypes: { - native: { - image: { - required: true, - sendId: false - }, - title: { - required: true, - sendId: true - }, - sponsoredBy: { - required: false, - sendId: true - }, - clickUrl: { - required: false, - sendId: true - }, - body: { - required: false, - sendId: true - }, - icon: { - required: false, - sendId: false - }, - privacyLink: { - required: true, - sendId: true - }, - } - }, - bids: [{ - bidder: 'aja', - params: { - asi: 'qxueUGliR' - } - }] } ]; ``` diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index 6274ad2bf1c..53f6460434f 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, deepClone, getDNT, generateUUID} from '../src/utils.js'; +import {deepAccess, deepClone, getDNT, generateUUID, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; -import {VIDEO} from '../src/mediaTypes.js'; +import {VIDEO, BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; const BIDDER_CODE = 'alkimi'; @@ -59,6 +59,9 @@ export const spec = { h: screen.height }, ortb2: { + site: { + keywords: bidderRequest.ortb2?.site?.keywords + }, at: bidderRequest.ortb2?.at, bcat: bidderRequest.ortb2?.bcat, wseat: bidderRequest.ortb2?.wseat @@ -113,7 +116,7 @@ export const spec = { // banner or video if (VIDEO === bid.mediaType) { - bid.vastXml = bid.ad; + bid.vastUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); } bid.meta = {}; @@ -126,21 +129,12 @@ export const spec = { }, onBidWon: function (bid) { - let winUrl; - if (bid.winUrl || bid.vastUrl) { - winUrl = bid.winUrl ? bid.winUrl : bid.vastUrl; - winUrl = winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); - } else if (bid.ad) { - let trackImg = bid.ad.match(/(?!^)/); - bid.ad = bid.ad.replace(trackImg[0], ''); - winUrl = trackImg[0].split('"')[1]; - winUrl = winUrl.replace(/\$%7BAUCTION_PRICE%7D/, bid.cpm); - } else { - return false; + if (BANNER == bid.mediaType && bid.winUrl) { + const winUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); + ajax(winUrl, null); + return true; } - - ajax(winUrl, null); - return true; + return false; } } diff --git a/modules/ampliffyBidAdapter.js b/modules/ampliffyBidAdapter.js new file mode 100644 index 00000000000..bcd28e5bcf1 --- /dev/null +++ b/modules/ampliffyBidAdapter.js @@ -0,0 +1,419 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {logError, logInfo, triggerPixel} from '../src/utils.js'; + +const BIDDER_CODE = 'ampliffy'; +const GVLID = 1258; +const DEFAULT_ENDPOINT = 'bidder.ampliffy.com'; +const TTL = 600; // Time-to-Live - how long (in seconds) Prebid can use this bid. +const LOG_PREFIX = 'AmpliffyBidder: '; + +function isBidRequestValid(bid) { + logInfo(LOG_PREFIX + 'isBidRequestValid: Code: ' + bid.adUnitCode + ': Param' + JSON.stringify(bid.params), bid.adUnitCode); + if (bid.params) { + if (!bid.params.placementId || !bid.params.format) return false; + + if (bid.params.format.toLowerCase() !== 'video' && bid.params.format.toLowerCase() !== 'display' && bid.params.format.toLowerCase() !== 'all') return false; + if (bid.params.format.toLowerCase() === 'video' && !bid.mediaTypes['video']) return false; + if (bid.params.format.toLowerCase() === 'display' && !bid.mediaTypes['banner']) return false; + + if (!bid.params.server || bid.params.server === '') { + const server = bid.params.type + bid.params.region + bid.params.adnetwork; + if (server && server !== '') bid.params.server = server; + else bid.params.server = DEFAULT_ENDPOINT; + } + return true; + } + return false; +} + +function manageConsentArguments(bidderRequest) { + let consent = null; + if (bidderRequest?.gdprConsent) { + consent = { + gdpr: bidderRequest.gdprConsent.gdprApplies ? '1' : '0', + }; + if (bidderRequest.gdprConsent.consentString) { + consent.consent_string = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + consent.addtl_consent = bidderRequest.gdprConsent.addtlConsent; + } + } + return consent; +} + +function buildRequests(validBidRequests, bidderRequest) { + const bidRequests = []; + for (const bidRequest of validBidRequests) { + for (const sizes of bidRequest.sizes) { + let extraParams = mergeParams(getDefaultParams(), bidRequest.params.extraParams); + // Apply GDPR parameters to request. + extraParams = mergeParams(extraParams, manageConsentArguments(bidderRequest)); + const serverURL = getServerURL(bidRequest.params.server, sizes, bidRequest.params.placementId, extraParams); + logInfo(LOG_PREFIX + serverURL, 'requests'); + extraParams.bidId = bidRequest.bidId; + bidRequests.push({ + method: 'GET', + url: serverURL, + data: extraParams, + bidRequest, + }); + } + logInfo(LOG_PREFIX + 'Building request from: ' + bidderRequest.url + ': ' + JSON.stringify(bidRequests), bidRequest.adUnitCode); + } + return bidRequests; +} +export function getDefaultParams() { + return { + ciu_szs: '1x1', + gdfp_req: '1', + env: 'vp', + output: 'xml_vast4', + unviewed_position_start: '1' + }; +} +export function mergeParams(params, extraParams) { + if (extraParams) { + for (const k in extraParams) { + params[k] = extraParams[k]; + } + } + return params; +} +export function paramsToQueryString(params) { + return Object.entries(params).filter(e => typeof e[1] !== 'undefined').map(e => { + if (e[1]) return encodeURIComponent(e[0]) + '=' + encodeURIComponent(e[1]); + else return encodeURIComponent(e[0]); + }).join('&'); +} +const getCacheBuster = () => Math.floor(Math.random() * (9999999999 - 1000000000)); + +// For testing purposes +let currentUrl = null; +export function getCurrentURL() { + if (!currentUrl) currentUrl = top.location.href; + return currentUrl; +} +export function setCurrentURL(url) { + currentUrl = url; +} +const getCurrentURLEncoded = () => encodeURIComponent(getCurrentURL()); +function getServerURL(server, sizes, iu, queryParams) { + const random = getCacheBuster(); + const size = sizes[0] + 'x' + sizes[1]; + let serverURL = '//' + server + '/gampad/ads'; + queryParams.sz = size; + queryParams.iu = iu; + queryParams.url = getCurrentURL(); + queryParams.description_url = getCurrentURL(); + queryParams.correlator = random; + + return serverURL; +} +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + + const bidResponse = {}; + let mediaType = 'video'; + if ( + bidRequest.bidRequest?.mediaTypes && + !bidRequest.bidRequest.mediaTypes['video'] + ) { + mediaType = 'banner'; + } + bidResponse.requestId = bidRequest.bidRequest.bidId; + bidResponse.width = bidRequest.bidRequest?.sizes[0][0]; + bidResponse.height = bidRequest.bidRequest?.sizes[0][1]; + bidResponse.ttl = TTL; + bidResponse.creativeId = 'ampCreativeID134'; + bidResponse.netRevenue = true; + bidResponse.mediaType = mediaType; + bidResponse.meta = { + advertiserDomains: [], + }; + let xmlStr = serverResponse.body; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + const xmlData = parseXML(xml, bidResponse); + logInfo(LOG_PREFIX + 'Response from: ' + bidRequest.url + ': ' + JSON.stringify(xmlData), bidRequest.bidRequest.adUnitCode); + if (xmlData.cpm < 0 || !xmlData.creativeURL || !xmlData.bidUp) { + return []; + } + bidResponse.cpm = xmlData.cpm; + bidResponse.currency = xmlData.currency; + + if (mediaType === 'video') { + logInfo(LOG_PREFIX + xmlData.creativeURL, 'requests'); + bidResponse.vastUrl = xmlData.creativeURL; + } else { + bidResponse.adUrl = xmlData.creativeURL; + } + if (xmlData.trackingUrl) { + bidResponse.vastImpUrl = xmlData.trackingUrl; + bidResponse.trackingUrl = xmlData.trackingUrl; + } + bidResponses.push(bidResponse); + return bidResponses; +} +const replaceMacros = (txt, cpm, bid) => { + const size = bid.width + 'x' + bid.height; + txt = txt.replaceAll('%%CACHEBUSTER%%', getCacheBuster()); + txt = txt.replaceAll('@@CACHEBUSTER@@', getCacheBuster()); + txt = txt.replaceAll('%%REFERER%%', getCurrentURLEncoded()); + txt = txt.replaceAll('@@REFERER@@', getCurrentURLEncoded()); + txt = txt.replaceAll('%%REFERRER_URL_UNESC%%', getCurrentURLEncoded()); + txt = txt.replaceAll('@@REFERRER_URL_UNESC@@', getCurrentURLEncoded()); + txt = txt.replaceAll('%%PRICE_ESC%%', encodePrice(cpm)); + txt = txt.replaceAll('@@PRICE_ESC@@', encodePrice(cpm)); + txt = txt.replaceAll('%%SIZES%%', size); + txt = txt.replaceAll('@@SIZES@@', size); + return txt; +} +const encodePrice = (price) => { + price = parseFloat(price); + const s = 116.54; + const c = 1; + const a = 1; + let encodedPrice = s * Math.log10(price + a) + c; + encodedPrice = Math.min(200, encodedPrice); + encodedPrice = Math.round(Math.max(1, encodedPrice)); + + // Format the encoded price with leading zeros if necessary + const formattedEncodedPrice = encodedPrice.toString().padStart(3, '0'); + + // Build the encoding key + const encodingKey = `H--${formattedEncodedPrice}`; + + return encodeURIComponent(`vch=${encodingKey}`); +}; + +function extractCT(xml) { + let ct = null; + try { + try { + const vastAdTagURI = xml.getElementsByTagName('VASTAdTagURI')[0] + if (vastAdTagURI) { + let url = null; + for (const childNode of vastAdTagURI.childNodes) { + if (childNode.nodeValue.trim().includes('http')) { + url = decodeURIComponent(childNode.nodeValue); + } + } + const urlParams = new URLSearchParams(url); + ct = urlParams.get('ct') + } + } catch (e) { + } + if (!ct) { + const geoExtensions = xml.querySelectorAll('Extension[type="geo"]'); + geoExtensions.forEach((geoExtension) => { + const countryElement = geoExtension.querySelector('Country'); + if (countryElement) { + ct = countryElement.textContent; + } + }); + } + } catch (e) {} + return ct; +} + +function extractCPM(htmlContent, ct, cpm) { + const cpmMapDiv = htmlContent.querySelectorAll('[cpmMap]')[0]; + if (cpmMapDiv) { + let cpmMapJSON = JSON.parse(cpmMapDiv.getAttribute('cpmMap')); + if ((cpmMapJSON)) { + if (cpmMapJSON[ct]) { + cpm = cpmMapJSON[ct]; + } else if (cpmMapJSON['default']) { + cpm = cpmMapJSON['default']; + } + } + } + return cpm; +} + +function extractCurrency(htmlContent, currency) { + const currencyDiv = htmlContent.querySelectorAll('[cpmCurrency]')[0]; + if (currencyDiv) { + const currencyValue = currencyDiv.getAttribute('cpmCurrency'); + if (currencyValue && currencyValue !== '') { + currency = currencyValue; + } + } + return currency; +} + +function extractCreativeURL(htmlContent, ct, cpm, bid) { + let creativeURL = null; + const creativeMap = htmlContent.querySelectorAll('[creativeMap]')[0]; + if (creativeMap) { + const creativeMapString = creativeMap.getAttribute('creativeMap'); + + const creativeMapJSON = JSON.parse(creativeMapString); + let defaultURL = null; + for (const url of Object.keys(creativeMapJSON)) { + const geo = creativeMapJSON[url]; + if (geo.includes(ct)) { + creativeURL = replaceMacros(url, cpm, bid); + } else if (geo.includes('default')) { + defaultURL = url; + } + } + if (!creativeURL && defaultURL) creativeURL = replaceMacros(defaultURL, cpm, bid); + } + return creativeURL; +} + +function extractSyncs(htmlContent) { + let userSyncsJSON = null; + const userSyncs = htmlContent.querySelectorAll('[userSyncs]')[0]; + if (userSyncs) { + const userSyncsString = userSyncs.getAttribute('userSyncs'); + + userSyncsJSON = JSON.parse(userSyncsString); + } + return userSyncsJSON; +} + +function extractTrackingURL(htmlContent, ret) { + const trackingUrlDiv = htmlContent.querySelectorAll('[bidder-tracking-url]')[0]; + if (trackingUrlDiv) { + const trackingUrl = trackingUrlDiv.getAttribute('bidder-tracking-url'); + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'parseXML: trackingUrl: ', trackingUrl) + ret.trackingUrl = trackingUrl; + } +} + +export function parseXML(xml, bid) { + const ret = { cpm: 0.001, currency: 'EUR', creativeURL: null, bidUp: false }; + const ct = extractCT(xml); + if (!ct) return ret; + + try { + if (ct) { + const companion = xml.getElementsByTagName('Companion')[0]; + const htmlResource = companion.getElementsByTagName('HTMLResource')[0]; + const htmlContent = document.createElement('html'); + htmlContent.innerHTML = htmlResource.textContent; + + ret.cpm = extractCPM(htmlContent, ct, ret.cpm); + ret.currency = extractCurrency(htmlContent, ret.currency); + ret.creativeURL = extractCreativeURL(htmlContent, ct, ret.cpm, bid); + extractTrackingURL(htmlContent, ret); + ret.bidUp = isAllowedToBidUp(htmlContent, getCurrentURL()); + ret.userSyncs = extractSyncs(htmlContent); + } + } catch (e) { + // eslint-disable-next-line no-console + logError(LOG_PREFIX + 'Error parsing XML', e); + } + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'parseXML RET:', ret); + + return ret; +} +export function isAllowedToBidUp(html, currentURL) { + currentURL = currentURL.split('?')[0]; // Remove parameters + let allowedToPush = false; + try { + const domainsMap = html.querySelectorAll('[domainMap]')[0]; + if (domainsMap) { + let domains = JSON.parse(domainsMap.getAttribute('domainMap')); + if (domains.domainMap) { + domains = domains.domainMap; + } + domains.forEach((d) => { + if (currentURL.includes(d) || d === 'all' || d === '*') allowedToPush = true; + }) + } else { + allowedToPush = true; + } + if (allowedToPush) { + const excludedURL = html.querySelectorAll('[excludedURLs]')[0]; + if (excludedURL) { + const excludedURLsString = domainsMap.getAttribute('excludedURLs'); + if (excludedURLsString !== '') { + let excluded = JSON.parse(excludedURLsString); + excluded.forEach((d) => { + if (currentURL.includes(d)) allowedToPush = false; + }) + } + } + } + } catch (e) { + // eslint-disable-next-line no-console + logError(LOG_PREFIX + 'isAllowedToBidUp', e); + } + return allowedToPush; +} + +function getSyncData(options, syncs) { + const ret = []; + if (syncs?.length) { + for (const sync of syncs) { + if (sync.type === 'syncImage' && options.pixelEnabled) { + ret.push({url: sync.url, type: 'image'}); + } else if (sync.type === 'syncIframe' && options.iframeEnabled) { + ret.push({url: sync.url, type: 'iframe'}); + } + } + } + return ret; +} + +function getUserSyncs(syncOptions, serverResponses) { + const userSyncs = []; + for (const serverResponse of serverResponses) { + if (serverResponse.body) { + try { + const xmlStr = serverResponse.body; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + const xmlData = parseXML(xml, {}); + if (xmlData.userSyncs) { + userSyncs.push(...getSyncData(syncOptions, xmlData.userSyncs)); + } + } catch (e) {} + } + } + return userSyncs; +} + +function onBidWon(bid) { + logInfo(`${LOG_PREFIX} WON AMPLIFFY`); + if (bid.trackingUrl) { + let url = bid.trackingUrl; + + // Replace macros with URL-encoded bid parameters + Object.keys(bid).forEach(key => { + const macroKey = `%%${key.toUpperCase()}%%`; + const value = encodeURIComponent(JSON.stringify(bid[key])); + url = url.split(macroKey).join(value); + }); + + triggerPixel(url, () => { + logInfo(`${LOG_PREFIX} send data success`); + }, + (e) => { + logError(`${LOG_PREFIX} send data error`, e); + }); + } +} +function onTimeOut() { + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'TIMEOUT'); +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ['ampliffy', 'amp', 'videoffy', 'publiffy'], + supportedMediaTypes: ['video', 'banner'], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeOut, + onBidWon, +}; + +registerBidder(spec); diff --git a/modules/ampliffyBidAdapter.md b/modules/ampliffyBidAdapter.md new file mode 100644 index 00000000000..a425d910582 --- /dev/null +++ b/modules/ampliffyBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +``` +Module Name: Ampliffy Bidder Adapter +Module Type: Bidder Adapter +Maintainer: bidder@ampliffy.com +``` + +# Description + +Connects to Ampliffy Ad server for bids. + +Ampliffy bid adapter supports Video currently, and has initial support for Banner. + +For more information about [Ampliffy](https://www.ampliffy.com/en/), please contact [info@ampliffy.com](info@ampliffy.com). + +# Sample Ad Unit: For Publishers +```javascript +var videoAdUnit = [ +{ + code: 'video1', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [{ + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: '1213213/example/vrutal_/', + format: 'video' + } + }] +}]; +``` + +``` diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index a773ac70559..6e14f65b0c8 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -14,20 +14,31 @@ import { } from '../src/utils.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import { fetch } from '../src/ajax.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.3.3'; +const VERSION = 'pba1.3.4'; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; -const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; +const TRACKING_BASE = 'https://1x1.a-mo.net/'; +const TRACKING_ENDPOINT = TRACKING_BASE + 'hbx/'; +const POST_TRACKING_ENDPOINT = TRACKING_BASE + 'e'; const AMUID_KEY = '__amuidpb'; function getLocation(request) { return parseUrl(request.refererInfo?.topmostLocation || window.location.href); } +function getTimeoutSize(timeoutData) { + if (timeoutData.sizes == null || timeoutData.sizes.length === 0) { + return [0, 0]; + } + + return timeoutData.sizes[0]; +} + const largestSize = (sizes, mediaTypes) => { const allSizes = sizes .concat(deepAccess(mediaTypes, `${BANNER}.sizes`, []) || []) @@ -149,7 +160,9 @@ function convertRequest(bid) { const tid = deepAccess(bid, 'params.tagId'); const au = - bid.params != null && typeof bid.params.adUnitId === 'string' && bid.params.adUnitId !== '' + bid.params != null && + typeof bid.params.adUnitId === 'string' && + bid.params.adUnitId !== '' ? bid.params.adUnitId : bid.adUnitCode; @@ -202,7 +215,10 @@ function isSyncEnabled(syncConfigP, syncType) { return false; } - if (syncConfig.bidders === '*' || (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1)) { + if ( + syncConfig.bidders === '*' || + (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1) + ) { return syncConfig.filter == null || syncConfig.filter === 'include'; } @@ -219,12 +235,17 @@ function getSyncSettings() { d: 0, l: 0, t: 0, - e: true + e: true, }; } - const settings = { d: syncConfig.syncDelay, l: syncConfig.syncsPerBidder, t: 0, e: syncConfig.syncEnabled } - const all = isSyncEnabled(syncConfig.filterSettings, 'all') + const settings = { + d: syncConfig.syncDelay, + l: syncConfig.syncsPerBidder, + t: 0, + e: syncConfig.syncEnabled, + }; + const all = isSyncEnabled(syncConfig.filterSettings, 'all'); if (all) { settings.t = SYNC_IMAGE & SYNC_IFRAME; @@ -256,12 +277,14 @@ function getGpp(bidderRequest) { return bidderRequest.gppConsent; } - return bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' }; + return ( + bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' } + ); } function buildReferrerInfo(bidderRequest) { if (bidderRequest.refererInfo == null) { - return { r: '', t: false, c: '', l: 0, s: [] } + return { r: '', t: false, c: '', l: 0, s: [] }; } const re = bidderRequest.refererInfo; @@ -272,7 +295,7 @@ function buildReferrerInfo(bidderRequest) { l: re.numIframes, s: re.stack, c: re.canonicalUrl, - } + }; } const isTrue = (boolValue) => @@ -358,28 +381,35 @@ export const spec = { return { data: payload, method: 'POST', + browsingTopics: true, url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT), withCredentials: true, }; }, - getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + getUserSyncs( + syncOptions, + serverResponses, + gdprConsent, + uspConsent, + gppConsent + ) { const qp = { gdpr_consent: enc(gdprConsent?.consentString || ''), gdpr: enc(gdprConsent?.gdprApplies ? 1 : 0), us_privacy: enc(uspConsent || ''), gpp: enc(gppConsent?.gppString || ''), - gpp_sid: enc(gppConsent?.applicableSections || '') + gpp_sid: enc(gppConsent?.applicableSections || ''), }; const iframeSync = { url: `https://prebid.a-mo.net/isyn?${formatQS(qp)}`, - type: 'iframe' + type: 'iframe', }; if (serverResponses == null || serverResponses.length === 0) { if (syncOptions.iframeEnabled) { - return [iframeSync] + return [iframeSync]; } return []; @@ -394,7 +424,10 @@ export const spec = { const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image'; if (syncOptions.iframeEnabled || pixelType === 'image') { - hasFrame = hasFrame || (pixelType === 'iframe') || (syncPixel.indexOf('cchain') !== -1) + hasFrame = + hasFrame || + pixelType === 'iframe' || + syncPixel.indexOf('cchain') !== -1; output.push({ url: syncPixel, type: pixelType, @@ -405,7 +438,7 @@ export const spec = { }); if (!hasFrame && output.length < 2) { - output.push(iframeSync) + output.push(iframeSync); } return output; @@ -470,19 +503,58 @@ export const spec = { aud: targetingData.requestId, a: targetingData.adUnitCode, c2: nestedQs(targetingData.adserverTargeting), + cn3: targetingData.timeToRespond, }); }, onTimeout(timeoutData) { - if (timeoutData == null) { + if (timeoutData == null || !timeoutData.length) { return; } - trackEvent('pbto', { - A: timeoutData.bidder, - bid: timeoutData.bidId, - a: timeoutData.adUnitCode, - cn: timeoutData.timeout, + let common = null; + const events = timeoutData.map((timeout) => { + const params = timeout.params || {}; + const size = getTimeoutSize(timeout); + const { domain, page, ref } = + timeout.ortb2 != null && timeout.ortb2.site != null + ? timeout.ortb2.site + : {}; + + if (common == null) { + common = { + do: domain, + u: page, + U: getUIDSafe(), + re: ref, + V: '$prebid.version$', + vg: '$$PREBID_GLOBAL$$', + }; + } + + return { + A: timeout.bidder, + mid: params.tagId, + a: params.adunitId || timeout.adUnitCode, + bid: timeout.bidId, + n: 'g_pbto', + aud: timeout.transactionId, + w: size[0], + h: size[1], + cn: timeout.timeout, + cn2: timeout.bidderRequestsCount, + cn3: timeout.bidderWinsCount, + }; + }); + + const payload = JSON.stringify({ c: common, e: events }); + fetch(POST_TRACKING_ENDPOINT, { + body: payload, + keepalive: true, + withCredentials: true, + method: 'POST' + }).catch((_e) => { + // do nothing; ignore errors }); }, diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index e1557d9c6d3..834df134c2e 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -96,8 +96,8 @@ export const spec = { payload.device = {}; payload.device.ua = navigator.userAgent; - payload.device.height = window.screen.width; - payload.device.width = window.screen.height; + payload.device.height = window.screen.height; + payload.device.width = window.screen.width; payload.device.dnt = _getDoNotTrack(); payload.device.language = navigator.language; diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index ae4b1a0d489..c6230d9f1e4 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,17 +1,10 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, getUniqueIdentifierStr, - getWindowFromDocument, isArray, isArrayOfNums, isEmpty, @@ -40,7 +33,10 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -114,6 +110,7 @@ export const spec = { { code: 'beintoo', gvlid: 618 }, { code: 'projectagora', gvlid: 1032 }, { code: 'uol', gvlid: 32 }, + { code: 'adzymic', gvlid: 32 }, ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -1031,7 +1028,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -1057,7 +1054,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration @@ -1142,7 +1139,7 @@ function outstreamRender(bid, doc) { hideSASIframe(bid.adUnitCode); // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index e569f04a2a8..704cffefb39 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -7,14 +7,14 @@ import { isArray, isFn, logWarn, - parseSizesInput, - tryAppendQueryString + 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 {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'aso'; const DEFAULT_SERVER_URL = 'https://srv.aso1.net'; diff --git a/modules/asteriobidAnalyticsAdapter.js b/modules/asteriobidAnalyticsAdapter.js new file mode 100644 index 00000000000..516a3a65667 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.js @@ -0,0 +1,336 @@ +import { generateUUID, getParameterByName, logError, logInfo, parseUrl } from '../src/utils.js' +import { ajaxBuilder } from '../src/ajax.js' +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import { getStorageManager } from '../src/storageManager.js' +import CONSTANTS from '../src/constants.json' +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js' +import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * asteriobidAnalyticsAdapter.js - analytics adapter for AsterioBid + */ +export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'asteriobid' }) +const DEFAULT_EVENT_URL = 'https://endpt.asteriobid.com/endpoint' +const analyticsType = 'endpoint' +const analyticsName = 'AsterioBid Analytics' +const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] +const _VERSION = 1 + +let ajax = ajaxBuilder(20000) +let initOptions +let auctionStarts = {} +let auctionTimeouts = {} +let sampling +let pageViewId +let flushInterval +let eventQueue = [] +let asteriobidAnalyticsEnabled = false + +let asteriobidAnalytics = Object.assign(adapter({ url: DEFAULT_EVENT_URL, analyticsType }), { + track({ eventType, args }) { + handleEvent(eventType, args) + } +}) + +asteriobidAnalytics.originEnableAnalytics = asteriobidAnalytics.enableAnalytics +asteriobidAnalytics.enableAnalytics = function (config) { + initOptions = config.options || {} + + pageViewId = initOptions.pageViewId || generateUUID() + sampling = initOptions.sampling || 1 + + if (Math.floor(Math.random() * sampling) === 0) { + asteriobidAnalyticsEnabled = true + flushInterval = setInterval(flush, 1000) + } else { + logInfo(`${analyticsName} isn't enabled because of sampling`) + } + + asteriobidAnalytics.originEnableAnalytics(config) +} + +asteriobidAnalytics.originDisableAnalytics = asteriobidAnalytics.disableAnalytics +asteriobidAnalytics.disableAnalytics = function () { + if (!asteriobidAnalyticsEnabled) { + return + } + flush() + clearInterval(flushInterval) + asteriobidAnalytics.originDisableAnalytics() +} + +function collectUtmTagData() { + let newUtm = false + let pmUtmTags = {} + try { + utmTags.forEach(function (utmKey) { + let utmValue = getParameterByName(utmKey) + if (utmValue !== '') { + newUtm = true + } + pmUtmTags[utmKey] = utmValue + }) + if (newUtm === false) { + utmTags.forEach(function (utmKey) { + let itemValue = storage.getDataFromLocalStorage(`pm_${utmKey}`) + if (itemValue && itemValue.length !== 0) { + pmUtmTags[utmKey] = itemValue + } + }) + } else { + utmTags.forEach(function (utmKey) { + storage.setDataInLocalStorage(`pm_${utmKey}`, pmUtmTags[utmKey]) + }) + } + } catch (e) { + logError(`${analyticsName} Error`, e) + pmUtmTags['error_utm'] = 1 + } + return pmUtmTags +} + +function collectPageInfo() { + const pageInfo = { + domain: window.location.hostname, + } + if (document.referrer) { + pageInfo.referrerDomain = parseUrl(document.referrer).hostname + } + + const refererInfo = getRefererInfo() + pageInfo.page = refererInfo.page + pageInfo.ref = refererInfo.ref + + return pageInfo +} + +function flush() { + if (!asteriobidAnalyticsEnabled) { + return + } + + if (eventQueue.length > 0) { + const data = { + pageViewId: pageViewId, + ver: _VERSION, + bundleId: initOptions.bundleId, + events: eventQueue, + utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), + sampling: sampling + } + eventQueue = [] + + if ('version' in initOptions) { + data.version = initOptions.version + } + if ('tcf_compliant' in initOptions) { + data.tcf_compliant = initOptions.tcf_compliant + } + if ('adUnitDict' in initOptions) { + data.adUnitDict = initOptions.adUnitDict; + } + if ('customParam' in initOptions) { + data.customParam = initOptions.customParam; + } + + const url = initOptions.url ? initOptions.url : DEFAULT_EVENT_URL + ajax( + url, + () => logInfo(`${analyticsName} sent events batch`), + _VERSION + ':' + JSON.stringify(data), + { + contentType: 'text/plain', + method: 'POST', + withCredentials: true + } + ) + } +} + +function trimAdUnit(adUnit) { + if (!adUnit) return adUnit + const res = {} + res.code = adUnit.code + res.sizes = adUnit.sizes + return res +} + +function trimBid(bid) { + if (!bid) return bid + const res = {} + res.auctionId = bid.auctionId + res.bidder = bid.bidder + res.bidderRequestId = bid.bidderRequestId + res.bidId = bid.bidId + res.crumbs = bid.crumbs + res.cpm = bid.cpm + res.currency = bid.currency + res.mediaTypes = bid.mediaTypes + res.sizes = bid.sizes + res.transactionId = bid.transactionId + res.adUnitCode = bid.adUnitCode + res.bidRequestsCount = bid.bidRequestsCount + res.serverResponseTimeMs = bid.serverResponseTimeMs + return res +} + +function trimBidderRequest(bidderRequest) { + if (!bidderRequest) return bidderRequest + const res = {} + res.auctionId = bidderRequest.auctionId + res.auctionStart = bidderRequest.auctionStart + res.bidderRequestId = bidderRequest.bidderRequestId + res.bidderCode = bidderRequest.bidderCode + res.bids = bidderRequest.bids && bidderRequest.bids.map(trimBid) + return res +} + +function handleEvent(eventType, eventArgs) { + if (!asteriobidAnalyticsEnabled) { + return + } + + try { + eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {} + } catch (e) { + // keep eventArgs as is + } + + const pmEvent = {} + pmEvent.timestamp = eventArgs.timestamp || Date.now() + pmEvent.eventType = eventType + + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeout = eventArgs.timeout + pmEvent.adUnits = eventArgs.adUnits && eventArgs.adUnits.map(trimAdUnit) + pmEvent.bidderRequests = eventArgs.bidderRequests && eventArgs.bidderRequests.map(trimBidderRequest) + auctionStarts[pmEvent.auctionId] = pmEvent.timestamp + auctionTimeouts[pmEvent.auctionId] = pmEvent.timeout + break + } + case CONSTANTS.EVENTS.AUCTION_END: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.end = eventArgs.end + pmEvent.start = eventArgs.start + pmEvent.adUnitCodes = eventArgs.adUnitCodes + pmEvent.bidsReceived = eventArgs.bidsReceived && eventArgs.bidsReceived.map(trimBid) + pmEvent.start = auctionStarts[pmEvent.auctionId] + pmEvent.end = Date.now() + break + } + case CONSTANTS.EVENTS.BID_ADJUSTMENT: { + break + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + pmEvent.bidders = eventArgs && eventArgs.map ? eventArgs.map(trimBid) : eventArgs + pmEvent.duration = auctionTimeouts[pmEvent.auctionId] + break + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.timeout = eventArgs.timeout + break + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.width = eventArgs.width + pmEvent.height = eventArgs.height + pmEvent.adId = eventArgs.adId + pmEvent.mediaType = eventArgs.mediaType + pmEvent.cpm = eventArgs.cpm + pmEvent.currency = eventArgs.currency + pmEvent.requestId = eventArgs.requestId + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeToRespond = eventArgs.timeToRespond + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.size = eventArgs.size + pmEvent.adserverTargeting = eventArgs.adserverTargeting + break + } + case CONSTANTS.EVENTS.BID_WON: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.adId = eventArgs.adId + pmEvent.adserverTargeting = eventArgs.adserverTargeting + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.height = eventArgs.height + pmEvent.mediaType = eventArgs.mediaType + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.cpm = eventArgs.cpm + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.size = eventArgs.size + pmEvent.width = eventArgs.width + pmEvent.currency = eventArgs.currency + pmEvent.bidder = eventArgs.bidder + break + } + case CONSTANTS.EVENTS.BIDDER_DONE: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.timeout = eventArgs.timeout + pmEvent.tid = eventArgs.tid + pmEvent.src = eventArgs.src + break + } + case CONSTANTS.EVENTS.SET_TARGETING: { + break + } + case CONSTANTS.EVENTS.REQUEST_BIDS: { + break + } + case CONSTANTS.EVENTS.ADD_AD_UNITS: { + break + } + case CONSTANTS.EVENTS.AD_RENDER_FAILED: { + pmEvent.bid = eventArgs.bid + pmEvent.message = eventArgs.message + pmEvent.reason = eventArgs.reason + break + } + default: + return + } + + sendEvent(pmEvent) +} + +function sendEvent(event) { + eventQueue.push(event) + logInfo(`${analyticsName} Event ${event.eventType}:`, event) + + if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + flush() + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: asteriobidAnalytics, + code: 'asteriobid' +}) + +asteriobidAnalytics.getOptions = function () { + return initOptions +} + +asteriobidAnalytics.flush = flush + +export default asteriobidAnalytics diff --git a/modules/asteriobidAnalyticsAdapter.md b/modules/asteriobidAnalyticsAdapter.md new file mode 100644 index 00000000000..524cf6e2721 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.md @@ -0,0 +1,41 @@ +# Overview + +Module Name: AsterioBid Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@asteriobid.com + +# Description +Analytics adapter for AsterioBid. Contact admin@asteriobid.com for information. + +# Test Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78' + } +}); + +``` + +# Advanced Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78', + version: 'v1', // configuration version for the comparison + adUnitDict: { // provide names of the ad units for better reporting + adunitid1: 'Top Banner', + adunitid2: 'Bottom Banner' + }, + customParam: { // provide custom parameters values that you want to collect and report + param1: 'value1', + param2: 'value2' + } + } +}); + +``` diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 9beb20d4f77..e716fe94c8b 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,8 +1,7 @@ import { _each, deepAccess, - formatQS, - getBidIdParameter, + formatQS, getBidIdParameter, getValue, isArray, isFn, diff --git a/modules/axisBidAdapter.js b/modules/axisBidAdapter.js new file mode 100644 index 00000000000..8d7f2dd04fd --- /dev/null +++ b/modules/axisBidAdapter.js @@ -0,0 +1,210 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'axis'; +const AD_URL = 'https://prebid.axis-marketplace.com/pbjs'; +const SYNC_URL = 'https://cs.axis-marketplace.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { integration, token } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + integration, + token, + bidId, + schain, + bidfloor + }; + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.pos = mediaTypes[BANNER].pos; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.pos = mediaTypes[VIDEO].pos; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + placement.context = mediaTypes[VIDEO].context; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (e) { + logError(e); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.integration && params.token); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + iabCat: deepAccess(bidderRequest, 'ortb2.site.cat'), + coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout || 3000, + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/axisBidAdapter.md b/modules/axisBidAdapter.md new file mode 100644 index 00000000000..d1625a56176 --- /dev/null +++ b/modules/axisBidAdapter.md @@ -0,0 +1,83 @@ +# Overview + +``` +Module Name: Axis Bidder Adapter +Module Type: Axis Bidder Adapter +Maintainer: help@axis-marketplace.com +``` + +# Description + +Connects to Axis exchange for bids. +Axis bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + } + ]; +``` diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index c5282c28cfc..ad52af824ee 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, getValue, isArray, logInfo, @@ -24,11 +23,11 @@ export const spec = { gvlid: TCF_VENDOR_ID, aliases: ['bp'], /** - * Test if the bid request is valid. - * - * @param {bid} : The Bid params - * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. - */ + * Test if the bid request is valid. + * + * @param {bid} : The Bid params + * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. + */ isBidRequestValid: function(bid) { const id = bid.params.accountId || bid.params.networkId; if (id === null || typeof id === 'undefined') { @@ -40,12 +39,12 @@ export const spec = { return bid.mediaTypes.banner !== null && typeof bid.mediaTypes.banner !== 'undefined'; }, /** - * Create a BeOp server request from a list of BidRequest - * - * @param {validBidRequests[], ...} : The array of validated bidRequests - * @param {... , bidderRequest} : Common params for each bidRequests - * @return ServerRequest Info describing the request to the BeOp's server - */ + * Create a BeOp server request from a list of BidRequest + * + * @param {validBidRequests[], ...} : The array of validated bidRequests + * @param {... , bidderRequest} : Common params for each bidRequests + * @return ServerRequest Info describing the request to the BeOp's server + */ buildRequests: function(validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); const firstPartyData = bidderRequest.ortb2 || {}; diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index d615e433cc0..6883b7cce2c 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,6 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getAdUnitSizes, parseSizesInput} from '../src/utils.js'; +import {parseSizesInput} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; diff --git a/modules/bidglassBidAdapter.js b/modules/bidglassBidAdapter.js index 3184372881b..a29976cfcb7 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -1,4 +1,4 @@ -import { _each, isArray, getBidIdParameter, deepClone, getUniqueIdentifierStr } from '../src/utils.js'; +import {_each, isArray, deepClone, getUniqueIdentifierStr, getBidIdParameter} from '../src/utils.js'; // import {config} from 'src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js index dc7731231ab..d2eba3f0f81 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/bizzclickBidAdapter.js @@ -1,322 +1,77 @@ -import {_map, deepAccess, deepSetValue, getDNT, logMessage, logWarn} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'bizzclick'; -const ACCOUNTID_MACROS = '[account_id]'; -const URL_ENDPOINT = `https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=${ACCOUNTID_MACROS}`; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; -const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' +const SOURCE_ID_MACRO = '[sourceid]'; +const ACCOUNT_ID_MACRO = '[accountid]'; +const HOST_MACRO = '[host]'; +const URL = `https://${HOST_MACRO}.bizzclick.com/bid?rtb_seat_id=${SOURCE_ID_MACRO}&secret_key=${ACCOUNT_ID_MACRO}&integration_type=prebidjs`; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_HOST = 'us-e-node1'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 20, }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.ext = { + [BIDDER_CODE]: { + accountId: bidRequest.params.accountId, + sourceId: bidRequest.params.sourceId, + host: bidRequest.params.host || DEFAULT_HOST, + } + } + return imp; }, - body: { - id: 4, - name: 'data', - type: 2 + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.test = config.getConfig('debug') ? 1 : 0; + if (!request.cur) request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + return request; }, - cta: { - id: 1, - type: 12, - name: 'data' + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; } -}; -const NATIVE_VERSION = '1.2'; +}); + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + isBidRequestValid: (bid) => { - return Boolean(bid.params.accountId) && Boolean(bid.params.placementId) + return Boolean(bid.params.sourceId) && Boolean(bid.params.accountId); }, - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (validBidRequests && validBidRequests.length === 0) return [] - let accuontId = validBidRequests[0].params.accountId; - const endpointURL = URL_ENDPOINT.replace(ACCOUNTID_MACROS, accuontId); - let winTop = window; - let location; - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let bids = []; - for (let bidRequest of validBidRequests) { - let impObject = prepareImpObject(bidRequest); - let data = { - id: bidRequest.bidId, - test: config.getConfig('debug') ? 1 : 0, - at: 1, - cur: ['USD'], - device: { - w: winTop.screen.width, - h: winTop.screen.height, - dnt: getDNT() ? 1 : 0, - language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', - }, - site: { - page: location.pathname, - host: location.host - }, - source: { - tid: bidRequest.ortb2Imp?.ext?.tid, - ext: { - schain: {} - } - }, - regs: { - coppa: config.getConfig('coppa') === true ? 1 : 0, - ext: {} - }, - user: { - ext: {} - }, - ext: { - ts: Date.now() - }, - tmax: bidRequest.timeout, - imp: [impObject], - }; - - let connection = navigator.connection || navigator.webkitConnection; - if (connection && connection.effectiveType) { - data.device.connectiontype = connection.effectiveType; - } - if (bidRequest) { - if (bidRequest.schain) { - deepSetValue(data, 'source.ext.schain', bidRequest.schain); - } - - if (bidRequest.gdprConsent && bidRequest.gdprConsent.gdprApplies) { - deepSetValue(data, 'regs.ext.gdpr', bidRequest.gdprConsent.gdprApplies ? 1 : 0); - deepSetValue(data, 'user.ext.consent', bidRequest.gdprConsent.consentString); - } - - if (bidRequest.uspConsent !== undefined) { - deepSetValue(data, 'regs.ext.us_privacy', bidRequest.uspConsent); - } - } - bids.push(data) - } + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return []; + const { sourceId, accountId } = validBidRequests[0].params; + const host = validBidRequests[0].params.host || 'USE'; + const endpointURL = URL.replace(HOST_MACRO, host || DEFAULT_HOST) + .replace(ACCOUNT_ID_MACRO, accountId) + .replace(SOURCE_ID_MACRO, sourceId); + const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); return { method: 'POST', url: endpointURL, - data: bids + data: request }; }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: (serverResponse) => { - if (!serverResponse || !serverResponse.body) return [] - let bizzclickResponse = serverResponse.body; - let bids = []; - for (let response of bizzclickResponse) { - let mediaType = response.seatbid[0].bid[0].ext && response.seatbid[0].bid[0].ext.mediaType ? response.seatbid[0].bid[0].ext.mediaType : BANNER; - let bid = { - requestId: response.id, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ttl: response.ttl || 1200, - currency: response.cur || 'USD', - netRevenue: true, - creativeId: response.seatbid[0].bid[0].crid, - dealId: response.seatbid[0].bid[0].dealid, - mediaType: mediaType - }; - - bid.meta = {}; - if (response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length > 0) { - bid.meta.advertiserDomains = response.seatbid[0].bid[0].adomain; - } - switch (mediaType) { - case VIDEO: - bid.vastXml = response.seatbid[0].bid[0].adm - bid.vastUrl = response.seatbid[0].bid[0].ext.vastUrl - break - case NATIVE: - bid.native = parseNative(response.seatbid[0].bid[0].adm) - break - default: - bid.ad = response.seatbid[0].bid[0].adm - } - bids.push(bid); + interpretResponse: (response, request) => { + if (response?.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; } - return bids; + return []; }, }; -/** - * Determine type of request - * - * @param bidRequest - * @param type - * @returns {boolean} - */ -const checkRequestType = (bidRequest, type) => { - return (typeof deepAccess(bidRequest, `mediaTypes.${type}`) !== 'undefined'); -} -const parseNative = admObject => { - const { assets, link, imptrackers, jstracker } = admObject.native; - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || undefined, - impressionTrackers: imptrackers || undefined, - javascriptTrackers: jstracker ? [ jstracker ] : undefined - }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; - } - }); - return result; -} -const prepareImpObject = (bidRequest) => { - let impObject = { - id: bidRequest.transactionId, - secure: 1, - ext: { - placementId: bidRequest.params.placementId - } - }; - if (checkRequestType(bidRequest, BANNER)) { - impObject.banner = addBannerParameters(bidRequest); - } - if (checkRequestType(bidRequest, VIDEO)) { - impObject.video = addVideoParameters(bidRequest); - } - if (checkRequestType(bidRequest, NATIVE)) { - impObject.native = { - ver: NATIVE_VERSION, - request: addNativeParameters(bidRequest) - }; - } - return impObject -}; -const addNativeParameters = bidRequest => { - let impObject = { - // TODO: top-level ID is not in ORTB native 1.2, is this intentional? - // (despite the name, this appears to be an ORTB native request - not an imp - object) - id: bidRequest.bidId, - ver: NATIVE_VERSION, - }; - const assets = _map(bidRequest.mediaTypes.native, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - const asset = { - required: bidParams.required & 1, - }; - if (props) { - asset.id = props.id; - let wmin, hmin; - let aRatios = bidParams.aspect_ratios; - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - } - if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); - wmin = sizes[0]; - hmin = sizes[1]; - } - asset[props.name] = {}; - if (bidParams.len) asset[props.name]['len'] = bidParams.len; - if (props.type) asset[props.name]['type'] = props.type; - if (wmin) asset[props.name]['wmin'] = wmin; - if (hmin) asset[props.name]['hmin'] = hmin; - return asset; - } - }).filter(Boolean); - impObject.assets = assets; - return impObject -} -const addBannerParameters = (bidRequest) => { - let bannerObject = {}; - const size = parseSizes(bidRequest, 'banner'); - bannerObject.w = size[0]; - bannerObject.h = size[1]; - return bannerObject; -}; -const parseSizes = (bid, mediaType) => { - let mediaTypes = bid.mediaTypes; - if (mediaType === 'video') { - let size = []; - if (mediaTypes.video && mediaTypes.video.w && mediaTypes.video.h) { - size = [ - mediaTypes.video.w, - mediaTypes.video.h - ]; - } else if (Array.isArray(deepAccess(bid, 'mediaTypes.video.playerSize')) && bid.mediaTypes.video.playerSize.length === 1) { - size = bid.mediaTypes.video.playerSize[0]; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0 && Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1) { - size = bid.sizes[0]; - } - return size; - } - let sizes = []; - if (Array.isArray(mediaTypes.banner.sizes)) { - sizes = mediaTypes.banner.sizes[0]; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizes = bid.sizes - } else { - logWarn('no sizes are setup or found'); - } - return sizes -} -const addVideoParameters = (bidRequest) => { - let videoObj = {}; - let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'] - for (let param of supportParamsList) { - if (bidRequest.mediaTypes.video[param] !== undefined) { - videoObj[param] = bidRequest.mediaTypes.video[param]; - } - } - const size = parseSizes(bidRequest, 'video'); - videoObj.w = size[0]; - videoObj.h = size[1]; - return videoObj; -} -const flatten = arr => { - return [].concat(...arr); -} + registerBidder(spec); diff --git a/modules/bizzclickBidAdapter.md b/modules/bizzclickBidAdapter.md index 6fc1bebf546..ad342f34e07 100644 --- a/modules/bizzclickBidAdapter.md +++ b/modules/bizzclickBidAdapter.md @@ -11,94 +11,99 @@ Maintainer: support@bizzclick.com Module that connects to BizzClick SSP demand sources # Test Parameters -``` - var adUnits = [{ - code: 'placementId', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]] - } - }, - bids: [{ - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - }] - }, - { - code: 'native_example', - // sizes: [[1, 1]], - mediaTypes: { - native: { - title: { - required: true, - len: 800 - }, - image: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - } - } - }, - bids: [ { - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - }] - }, - { - code: 'video1', - sizes: [640,480], - mediaTypes: { video: { - minduration:0, - maxduration:999, - boxingallowed:1, - skip:0, - mimes:[ - 'application/javascript', - 'video/mp4' - ], - w:1920, - h:1080, - protocols:[ - 2 - ], - linearity:1, - api:[ - 1, - 2 - ] - } }, +```js +const adUnits = [ + { + code: "placementId", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, + { + code: "native_example", + // sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 800, + }, + image: { + required: true, + len: 80, + }, + sponsoredBy: { + required: true, + }, + clickUrl: { + required: true, + }, + privacyLink: { + required: false, + }, + body: { + required: true, + }, + icon: { + required: true, + sizes: [50, 50], + }, + }, + }, bids: [ - { - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - } - ] - } - ]; -``` \ No newline at end of file + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, + { + code: "video1", + sizes: [640, 480], + mediaTypes: { + video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: ["application/javascript", "video/mp4"], + w: 1920, + h: 1080, + protocols: [2], + linearity: 1, + api: [1, 2], + }, + }, + bids: [ + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, +]; +``` diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 9a9d74d14c1..debe956d7eb 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -2,8 +2,9 @@ // eslint-disable-next-line prebid/validate-imports import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' -import {_each, deepAccess, deepSetValue} from '../src/utils.js' +import { _each, deepAccess, deepSetValue, getWindowSelf, getWindowTop } from '../src/utils.js' export const BIDDER_CODE = 'bliink' +export const GVL_ID = 658 export const BLIINK_ENDPOINT_ENGINE = 'https://engine.bliink.io/prebid' export const BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME = 'https://tag.bliink.io/usersync.html' @@ -12,9 +13,10 @@ export const META_DESCRIPTION = 'description' const VIDEO = 'video' const BANNER = 'banner' - +window.bliinkBid = window.bliinkBid || {}; const supportedMediaTypes = [BANNER, VIDEO] const aliasBidderCode = ['bk'] +const CURRENCY = 'EUR'; /** * @description get coppa value from config @@ -23,6 +25,36 @@ function getCoppa() { return config.getConfig('coppa') === true ? 1 : 0; } +/** + * Retrieves the effective connection type from the browser's Navigator API. + * @returns {string} The effective connection type or 'unsupported' if unavailable. + */ +export function getEffectiveConnectionType() { + /** + * The effective connection type obtained from the browser's Navigator API. + * @type {string|undefined} + */ + const navigatorEffectiveType = navigator?.connection?.effectiveType; + + if (navigatorEffectiveType) { + return navigatorEffectiveType; + } + + return 'unsupported'; +} + +/** + * Retrieves the user IDs as EIDs from the first valid bid request. + * + * @param {Array} validBidRequests - Array of valid bid requests + * @returns {Array|undefined} - Array of user IDs as EIDs, or undefined if not found + */ +export function getUserIds(validBidRequests) { + /** @type {Object} */ + if (validBidRequests?.[0]?.userIdAsEids) { + return validBidRequests[0].userIdAsEids; + } +} export function getMetaList(name) { if (!name || name.length === 0) return [] @@ -91,6 +123,35 @@ export function getKeywords() { return []; } +function canAccessTopWindow() { + try { + if (getWindowTop().location.href) { + return true; + } + } catch (error) { + return false; + } +} + +/** + * domLoading feature is computed on window.top if reachable. + */ +export function getDomLoadingDuration() { + let domLoadingDuration = -1; + let performance; + + performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance; + + if (performance && performance.timing && performance.timing.navigationStart > 0) { + const val = performance.timing.domLoading - performance.timing.navigationStart; + if (val > 0) { + domLoadingDuration = val; + } + } + + return domLoadingDuration; +} + /** * @param bidRequest * @return {({cpm, netRevenue: boolean, requestId, width: number, currency, ttl: number, creativeId, height: number}&{mediaType: string, vastXml})|null} @@ -120,7 +181,7 @@ export const buildBid = (bidResponse) => { } return Object.assign(bid, { cpm: bidResponse.price, - currency: bidResponse.currency || 'EUR', + currency: bidResponse.currency || CURRENCY, creativeId: deepAccess(bidResponse, 'extras.deal_id'), requestId: deepAccess(bidResponse, 'extras.transaction_id'), width: deepAccess(bidResponse, `creative.${bid.mediaType}.width`) || 1, @@ -149,29 +210,59 @@ export const isBidRequestValid = (bid) => { */ export const buildRequests = (validBidRequests, bidderRequest) => { if (!validBidRequests || !bidderRequest || !bidderRequest.bids) return null - + const domLoadingDuration = getDomLoadingDuration().toString(); const tags = bidderRequest.bids.map((bid) => { - return { + let bidFloor; + const sizes = bid.sizes.map((size) => ({ w: size[0], h: size[1] })); + const mediaTypes = Object.keys(bid.mediaTypes) + if (typeof bid.getFloor === 'function') { + bidFloor = bid.getFloor({ + currency: CURRENCY, + mediaType: mediaTypes[0], + size: sizes[0] + }); + } + const id = bid.params.tagId + const request = { sizes: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), - id: bid.params.tagId, + id, // TODO: bidId is globally unique, is it a good choice for transaction ID (vs ortb2Imp.ext.tid)? transactionId: bid.bidId, - mediaTypes: Object.keys(bid.mediaTypes), + mediaTypes: mediaTypes, imageUrl: deepAccess(bid, 'params.imageUrl', ''), - }; + videoUrl: deepAccess(bid, 'params.videoUrl', ''), + refresh: (window.bliinkBid[id] = (window.bliinkBid[id] ?? -1) + 1) || undefined, + } + if (bidFloor) { + request.bidFloor = bidFloor + } + return request; }); let request = { tags, pageTitle: document.title, - pageUrl: deepAccess(bidderRequest, 'refererInfo.page'), + pageUrl: deepAccess(bidderRequest, 'refererInfo.page').replace(/\?.*$/, ''), pageDescription: getMetaValue(META_DESCRIPTION), keywords: getKeywords().join(','), + ect: getEffectiveConnectionType(), }; + const schain = deepAccess(validBidRequests[0], 'schain') + const eids = getUserIds(validBidRequests) + const device = bidderRequest.ortb2?.device if (schain) { request.schain = schain } + if (domLoadingDuration > -1) { + request.domLoadingDuration = domLoadingDuration + } + if (device) { + request.device = device + } + if (eids) { + request.eids = eids + } const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { request.gdpr = true @@ -183,7 +274,6 @@ export const buildRequests = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'uspConsent', bidderRequest.uspConsent); } - return { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, @@ -254,6 +344,7 @@ const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent) => */ export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: aliasBidderCode, supportedMediaTypes: supportedMediaTypes, isBidRequestValid, diff --git a/modules/blueconicRtdProvider.js b/modules/blueconicRtdProvider.js index b6eb9374671..bf0c457bebd 100644 --- a/modules/blueconicRtdProvider.js +++ b/modules/blueconicRtdProvider.js @@ -19,9 +19,9 @@ export const RTD_LOCAL_NAME = 'bcPrebidData'; export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** -* Try parsing stringified array of data. -* @param {String} data -*/ + * Try parsing stringified array of data. + * @param {String} data + */ function parseJson(data) { try { return JSON.parse(data); diff --git a/modules/boldwinBidAdapter.js b/modules/boldwinBidAdapter.js index 3915df8b976..c7def383b5e 100644 --- a/modules/boldwinBidAdapter.js +++ b/modules/boldwinBidAdapter.js @@ -5,7 +5,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'boldwin'; const AD_URL = 'https://ssp.videowalldirect.com/pbjs'; -const SYNC_URL = 'https://cs.videowalldirect.com' +const SYNC_URL = 'https://sync.videowalldirect.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -80,6 +80,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index bd7a33ff037..20b1125cee9 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -72,10 +72,10 @@ function checkConsent (userConsent) { } /** -* Add event- listeners to hook in to brandmetrics events -* @param {Object} reqBidsConfigObj -* @param {function} callback -*/ + * Add event- listeners to hook in to brandmetrics events + * @param {Object} reqBidsConfigObj + * @param {function} callback + */ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { const callBidTargeting = (event) => { if (event.available && event.conf) { @@ -139,8 +139,8 @@ function initializeBrandmetrics(scriptId) { } /** -* Hook in to brandmetrics creative_in_view- event and emit billable- event for creatives measured by brandmetrics. -*/ + * Hook in to brandmetrics creative_in_view- event and emit billable- event for creatives measured by brandmetrics. + */ function initializeBillableEvents() { if (!billableEventsInitialized) { window._brandmetrics.push({ diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index c4cc5394a03..1fa1dac4e95 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -1,4 +1,17 @@ -import { getBidIdParameter, _each, isArray, getWindowTop, getUniqueIdentifierStr, deepSetValue, logError, logWarn, createTrackPixelHtml, getWindowSelf, isFn, isPlainObject } from '../src/utils.js'; +import { + _each, + isArray, + getWindowTop, + getUniqueIdentifierStr, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/brightcomSSPBidAdapter.js b/modules/brightcomSSPBidAdapter.js index b85a01c8fc7..4750881da40 100644 --- a/modules/brightcomSSPBidAdapter.js +++ b/modules/brightcomSSPBidAdapter.js @@ -1,5 +1,4 @@ import { - getBidIdParameter, isArray, getWindowTop, getUniqueIdentifierStr, @@ -9,7 +8,7 @@ import { createTrackPixelHtml, getWindowSelf, isFn, - isPlainObject, + isPlainObject, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 4a61f40600d..1f37c7c5bc0 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -88,6 +88,7 @@ export function collectData() { let predictorData = { ...{ sk: _moduleParams.siteKey, + pk: _moduleParams.pubKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, url: `${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`, @@ -134,7 +135,6 @@ function getRTD(auc) { const adSlot = getSlotByCode(uc); const identifier = adSlot ? getMacroId(_browsiData['pmd'], adSlot) : uc; const _pd = _bp[identifier]; - rp[uc] = getKVObject(-1); if (!_pd) { return rp } @@ -275,7 +275,7 @@ function getPredictionsFromServer(url) { if (req.status === 200) { try { const data = JSON.parse(response); - if (data && data.p && data.kn) { + if (data) { setData({p: data.p, kn: data.kn, pmd: data.pmd, bet: data.bet}); } else { setData({}); diff --git a/modules/bucksenseBidAdapter.js b/modules/bucksenseBidAdapter.js index 7b6c3911ea1..ecf27de997d 100644 --- a/modules/bucksenseBidAdapter.js +++ b/modules/bucksenseBidAdapter.js @@ -8,6 +8,7 @@ const URL = 'https://directo.prebidserving.com/prebidjs/'; export const spec = { code: BIDDER_CODE, + gvlid: 235, supportedMediaTypes: [BANNER], /** @@ -15,7 +16,7 @@ export const spec = { * * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. - */ + */ isBidRequestValid: function (bid) { logInfo(WHO + ' isBidRequestValid() - INPUT bid:', bid); if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -28,10 +29,10 @@ export const spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { logInfo(WHO + ' buildRequests() - INPUT validBidRequests:', validBidRequests, 'INPUT bidderRequest:', bidderRequest); @@ -73,7 +74,7 @@ export const spec = { * * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. - */ + */ interpretResponse: function (serverResponse, request) { logInfo(WHO + ' interpretResponse() - INPUT serverResponse:', serverResponse, 'INPUT request:', request); diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index 22ed0590e05..e73564dacdb 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isPlainObject, @@ -277,7 +276,7 @@ export const spec = { let isVideo = !!bid.mediaTypes.video; let data = { id: bid.bidId, - tid: bid.transactionId, + tid: bid.ortb2Imp?.ext?.tid, tagid, secure }; @@ -297,7 +296,7 @@ export const spec = { }); let cadentData = { - id: bidderRequest.auctionId, + id: bidderRequest.auctionId ?? bidderRequest.bidderRequestId, imp: cadentImps, device, site, @@ -374,7 +373,7 @@ export const spec = { } return cadentBidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncs = []; const consentParams = []; if (syncOptions.iframeEnabled) { @@ -390,6 +389,14 @@ export const spec = { if (uspConsent && typeof uspConsent.consentString === 'string') { consentParams.push(`usp=${uspConsent.consentString}`); } + if (gppConsent && typeof gppConsent === 'object') { + if (gppConsent.gppString && typeof gppConsent.gppString === 'string') { + consentParams.push(`gpp=${gppConsent.gppString}`); + } + if (gppConsent.applicableSections && typeof gppConsent.applicableSections === 'object') { + consentParams.push(`gpp_sid=${gppConsent.applicableSections}`); + } + } if (consentParams.length > 0) { url = url + '?' + consentParams.join('&'); } diff --git a/modules/codefuelBidAdapter.js b/modules/codefuelBidAdapter.js index 2548b20189b..4fef3fb2494 100644 --- a/modules/codefuelBidAdapter.js +++ b/modules/codefuelBidAdapter.js @@ -10,11 +10,11 @@ export const spec = { supportedMediaTypes: [ BANNER ], aliases: ['ex'], // short code /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { if (bid.nativeParams) { return false; @@ -22,11 +22,11 @@ export const spec = { return !!(bid.params.placementId || (bid.params.member && bid.params.invCode)); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const page = bidderRequest.refererInfo.page; const domain = bidderRequest.refererInfo.domain; @@ -78,11 +78,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: (serverResponse, { bids }) => { if (!serverResponse.body) { return []; @@ -116,12 +116,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { return []; } diff --git a/modules/coinzillaBidAdapter.js b/modules/coinzillaBidAdapter.js index 15731423c49..c7d8fa5797c 100644 --- a/modules/coinzillaBidAdapter.js +++ b/modules/coinzillaBidAdapter.js @@ -77,7 +77,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, - ttl: bidRequest.timeout, + ttl: response.timeout, referrer: referrer, ad: response.ad, mediaType: response.mediaType, diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 127b049bc99..87ac96f2131 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -3,23 +3,23 @@ import { BANNER } from '../src/mediaTypes.js'; // import { logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; const BIDDER_CODE = 'conceptx'; -let ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; +const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; // const LOG_PREFIX = 'ConceptX: '; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { - return !!(bid.bidId); + return !!(bid.bidId && bid.params.site && bid.params.adunit); }, buildRequests: function (validBidRequests, bidderRequest) { // logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); const requests = []; - + let requestUrl = `${ENDPOINT_URL}` if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - ENDPOINT_URL += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; - ENDPOINT_URL += '&consentString=' + bidderRequest.gdprConsent.consentString; + requestUrl += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; + requestUrl += '&consentString=' + bidderRequest.gdprConsent.consentString; } for (var i = 0; i < validBidRequests.length; i++) { const requestParent = { adUnits: [], meta: {} }; @@ -33,7 +33,7 @@ export const spec = { requestParent.adUnits.push(adUnit); requests.push({ method: 'POST', - url: ENDPOINT_URL, + url: requestUrl, options: { withCredentials: false, }, @@ -51,6 +51,9 @@ export const spec = { return bidResponses } const firstBid = bidResponsesFromServer[0] + if (!firstBid) { + return bidResponses + } const firstSeat = firstBid.ads[0] const bidResponse = { requestId: firstSeat.requestId, diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index 7042c895bfb..bc2c4555c54 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -49,6 +49,7 @@ export const spec = { uspConsent: bidderRequest.uspConsent, gdprConsent: bidderRequest.gdprConsent, gppConsent: bidderRequest.gppConsent, + tdid: getTdid(bidderRequest, validBidRequests), } }; @@ -263,3 +264,11 @@ function getOffset(el) { }; } } + +function getTdid(bidderRequest, validBidRequests) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return null; + } + + return deepAccess(validBidRequests[0], 'userId.tdid') || null; +} diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js index df56ad580bc..7524cd4e194 100644 --- a/modules/connatixBidAdapter.js +++ b/modules/connatixBidAdapter.js @@ -117,13 +117,12 @@ export const spec = { interpretResponse: (serverResponse) => { const responseBody = serverResponse.body; const bids = responseBody.Bids; - const playerId = responseBody.PlayerId; - const customerId = responseBody.CustomerId; - if (!isArray(bids) || !playerId || !customerId) { + if (!isArray(bids)) { return []; } + const referrer = responseBody.Referrer; return bids.map(bidResponse => ({ requestId: bidResponse.RequestId, cpm: bidResponse.Cpm, @@ -134,8 +133,8 @@ export const spec = { width: bidResponse.Width, height: bidResponse.Height, creativeId: bidResponse.CreativeId, - referrer: bidResponse.Referrer, ad: bidResponse.Ad, + referrer: referrer, })); }, diff --git a/modules/connatixBidAdapter.md b/modules/connatixBidAdapter.md index 7ac04a64245..595c294e311 100644 --- a/modules/connatixBidAdapter.md +++ b/modules/connatixBidAdapter.md @@ -9,7 +9,24 @@ Maintainer: prebid_integration@connatix.com # Description Connects to Connatix demand source to fetch bids. -Please use ```connatix``` as the bidder code. +Please use ```connatix``` as the bidder code. + +# Configuration +Connatix requires that ```iframe``` is used for user syncing. + +Example configuration: +``` +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: '*', // represents all bidders + filter: 'include' + } + } + } +}); +``` # Test Parameters ``` diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index e1c5b427264..35a77a9d72d 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -10,7 +10,7 @@ import {submodule} from '../src/hook.js'; import {includes} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {formatQS, isPlainObject, logError, parseUrl} from '../src/utils.js'; +import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -26,6 +26,16 @@ const PLACEHOLDER = '__PIXEL_ID__'; const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut'; const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid']; +const O_AND_O_DOMAINS = [ + 'yahoo.com', + 'aol.com', + 'aol.ca', + 'aol.de', + 'aol.co.uk', + 'engadget.com', + 'techcrunch.com', + 'autoblog.com', +]; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @@ -104,9 +114,11 @@ function syncLocalStorageToCookie() { } function isStale(storedIdData) { - if (isPlainObject(storedIdData) && storedIdData.lastSynced && - (storedIdData.lastSynced + VALID_ID_DURATION) <= Date.now()) { + if (isOAndOTraffic()) { return true; + } else if (isPlainObject(storedIdData) && storedIdData.lastSynced) { + const validTTL = storedIdData.ttl || VALID_ID_DURATION; + return storedIdData.lastSynced + validTTL <= Date.now(); } return false; } @@ -127,6 +139,17 @@ function getSiteHostname() { return pageInfo.hostname; } +function isOAndOTraffic() { + let referer = getRefererInfo().ref; + + if (referer) { + referer = parseUrl(referer).hostname; + const subDomains = referer.split('.'); + referer = subDomains.slice(subDomains.length - 2, subDomains.length).join('.'); + } + return O_AND_O_DOMAINS.indexOf(referer) >= 0; +} + /** @type {Submodule} */ export const connectIdSubmodule = { /** @@ -238,6 +261,13 @@ export const connectIdSubmodule = { responseObj.puid = params.puid || responseObj.puid; responseObj.lastSynced = Date.now(); responseObj.lastUsed = Date.now(); + if (isNumber(responseObj.ttl)) { + let validTTLMiliseconds = responseObj.ttl * 60 * 60 * 1000; + if (validTTLMiliseconds > VALID_ID_DURATION) { + validTTLMiliseconds = VALID_ID_DURATION; + } + responseObj.ttl = validTTLMiliseconds; + } storeObject(responseObj); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index d5665b318be..b40ef30f6bc 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -1,7 +1,9 @@ -import { deepSetValue, convertTypes, tryAppendQueryString, logWarn } from '../src/utils.js'; +import { deepSetValue, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 69fc5789953..f696ce25902 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -70,13 +70,18 @@ export class GPPClient { * - a promise to GPP data. */ static init(mkCmp = cmpClient) { - if (this.INST == null) { - this.INST = this.ping(mkCmp).catch(e => { - this.INST = null; + let inst = this.INST; + if (!inst) { + let err; + const reset = () => err && (this.INST = null); + inst = this.INST = this.ping(mkCmp).catch(e => { + err = true; + reset(); throw e; }); + reset(); } - return this.INST.then(([client, pingData]) => [ + return inst.then(([client, pingData]) => [ client, client.initialized ? client.refresh() : client.init(pingData) ]); @@ -247,7 +252,7 @@ class GPP10Client extends GPPClient { getGPPData(pingData) { const parsedSections = GreedyPromise.all( - pingData.supportedAPIs.map((api) => this.cmp({ + (pingData.supportedAPIs || pingData.apiSupport || []).map((api) => this.cmp({ command: 'getSection', parameter: api }).catch(err => { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index fb65a76c87b..1218c3724f4 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -90,7 +90,7 @@ function lookupUspConsent({onSuccess, onError}) { cmp({ command: 'registerDeletion', - callback: adapterManager.callDataDeletionRequest + callback: (res, success) => (success == null || success) && adapterManager.callDataDeletionRequest(res) }).catch(e => { logError('Error invoking CMP `registerDeletion`:', e); }); diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index c78ff7cdf51..696549a67dc 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -191,17 +191,20 @@ export const spec = { if (syncOptions.iframeEnabled) { if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); } else { - syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); } } if (gppConsent && gppConsent.gppString) { - syncUrl = appendUrlParam(syncUrl, `gpp=${gppConsent.gppString}&gpp_sid=${gppConsent.applicableSections}`); + syncUrl = appendUrlParam(syncUrl, `gpp=${encodeURIComponent(gppConsent.gppString)}`); + if (gppConsent.applicableSections && gppConsent.applicableSections.length > 0) { + syncUrl = appendUrlParam(syncUrl, `gpp_sid=${encodeURIComponent(gppConsent.applicableSections.join(','))}`); + } } - if (uspConsent && uspConsent.consentString) { - syncUrl = appendUrlParam(syncUrl, `us_privacy=${uspConsent.consentString}`); + if (uspConsent) { + syncUrl = appendUrlParam(syncUrl, `us_privacy=${encodeURIComponent(uspConsent)}`); } if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { diff --git a/modules/contxtfulRtdProvider.js b/modules/contxtfulRtdProvider.js new file mode 100644 index 00000000000..6d4b2a2ce29 --- /dev/null +++ b/modules/contxtfulRtdProvider.js @@ -0,0 +1,150 @@ +/** + * Contxtful Technologies Inc + * This RTD module provides receptivity feature that can be accessed using the + * getReceptivity() function. The value returned by this function enriches the ad-units + * that are passed within the `getTargetingData` functions and GAM. + */ + +import { submodule } from '../src/hook.js'; +import { + logInfo, + logError, + isStr, + isEmptyStr, + buildUrl, +} from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; + +const MODULE_NAME = 'contxtful'; +const MODULE = `${MODULE_NAME}RtdProvider`; + +const CONTXTFUL_RECEPTIVITY_DOMAIN = 'api.receptivity.io'; + +let initialReceptivity = null; +let contxtfulModule = null; + +/** + * Init function used to start sub module + * @param { { params: { version: String, customer: String, hostname: String } } } config + * @return { Boolean } + */ +function init(config) { + logInfo(MODULE, 'init', config); + initialReceptivity = null; + contxtfulModule = null; + + try { + const {version, customer, hostname} = extractParameters(config); + initCustomer(version, customer, hostname); + return true; + } catch (error) { + logError(MODULE, error); + return false; + } +} + +/** + * Extract required configuration for the sub module. + * validate that all required configuration are present and are valid. + * Throws an error if any config is missing of invalid. + * @param { { params: { version: String, customer: String, hostname: String } } } config + * @return { { version: String, customer: String, hostname: String } } + * @throws params.{name} should be a non-empty string + */ +function extractParameters(config) { + const version = config?.params?.version; + if (!isStr(version) || isEmptyStr(version)) { + throw Error(`${MODULE}: params.version should be a non-empty string`); + } + + const customer = config?.params?.customer; + if (!isStr(customer) || isEmptyStr(customer)) { + throw Error(`${MODULE}: params.customer should be a non-empty string`); + } + + const hostname = config?.params?.hostname || CONTXTFUL_RECEPTIVITY_DOMAIN; + + return {version, customer, hostname}; +} + +/** + * Initialize sub module for a customer. + * This will load the external resources for the sub module. + * @param { String } version + * @param { String } customer + * @param { String } hostname + */ +function initCustomer(version, customer, hostname) { + const CONNECTOR_URL = buildUrl({ + protocol: 'https', + host: hostname, + pathname: `/${version}/prebid/${customer}/connector/p.js`, + }); + + const externalScript = loadExternalScript(CONNECTOR_URL, MODULE_NAME); + addExternalScriptEventListener(externalScript); +} + +/** + * Add event listener to the script tag for the expected events from the external script. + * @param { HTMLScriptElement } script + */ +function addExternalScriptEventListener(script) { + if (!script) { + return; + } + + script.addEventListener('initialReceptivity', ({ detail }) => { + let receptivityState = detail?.ReceptivityState; + if (isStr(receptivityState) && !isEmptyStr(receptivityState)) { + initialReceptivity = receptivityState; + } + }); + + script.addEventListener('rxEngineIsReady', ({ detail: api }) => { + contxtfulModule = api; + }); +} + +/** + * Return current receptivity. + * @return { { ReceptivityState: String } } + */ +function getReceptivity() { + return { + ReceptivityState: contxtfulModule?.GetReceptivity()?.ReceptivityState || initialReceptivity + }; +} + +/** + * Set targeting data for ad server + * @param { [String] } adUnits + * @param {*} _config + * @param {*} _userConsent + * @return {{ code: { ReceptivityState: String } }} + */ +function getTargetingData(adUnits, _config, _userConsent) { + logInfo(MODULE, 'getTargetingData'); + if (!adUnits) { + return {}; + } + + const receptivity = getReceptivity(); + if (!receptivity?.ReceptivityState) { + return {}; + } + + return adUnits.reduce((targets, code) => { + targets[code] = receptivity; + return targets; + }, {}); +} + +export const contxtfulSubmodule = { + name: MODULE_NAME, + init, + extractParameters, + getTargetingData, +}; + +submodule('realTimeData', contxtfulSubmodule); diff --git a/modules/contxtfulRtdProvider.md b/modules/contxtfulRtdProvider.md new file mode 100644 index 00000000000..dfefca2067a --- /dev/null +++ b/modules/contxtfulRtdProvider.md @@ -0,0 +1,65 @@ +# Overview + +**Module Name:** Contxtful RTD Provider +**Module Type:** RTD Provider +**Maintainer:** [prebid@contxtful.com](mailto:prebid@contxtful.com) + +# Description + +The Contxtful RTD module offers a unique feature—Receptivity. Receptivity is an efficiency metric, enabling the qualification of any instant in a session in real time based on attention. The core idea is straightforward: the likelihood of an ad’s success increases when it grabs attention and is presented in the right context at the right time. + +To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please contact [prebid@contxtful.com](mailto:prebid@contxtful.com). + +# Configuration + +## Build Instructions + +To incorporate this module into your `prebid.js`, compile the module using the following command: + +```sh +gulp build --modules=contxtfulRtdProvider, +``` + +## Module Configuration + +Configure the `contxtfulRtdProvider` by passing the required settings through the `setConfig` function in `prebid.js`. + +```js +import pbjs from 'prebid.js'; + +pbjs.setConfig({ + "realTimeData": { + "auctionDelay": 1000, + "dataProviders": [ + { + "name": "contxtful", + "waitForIt": true, + "params": { + "version": "", + "customer": "" + } + } + ] + } +}); +``` + +### Configuration Parameters + +| Name | Type | Scope | Description | +|------------|----------|----------|-------------------------------------------| +| `version` | `string` | Required | Specifies the API version of Contxtful. | +| `customer` | `string` | Required | Your unique customer identifier. | + +# Usage + +The `contxtfulRtdProvider` module loads an external JavaScript file and authenticates with Contxtful APIs. The `getTargetingData` function then adds a `ReceptivityState` to each ad slot, which can have one of two values: `Receptive` or `NonReceptive`. + +```json +{ + "adUnitCode1": { "ReceptivityState": "Receptive" }, + "adUnitCode2": { "ReceptivityState": "NonReceptive" } +} +``` + +This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. \ No newline at end of file diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index fd436e51461..bef65a43616 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -3,22 +3,21 @@ import { isStr, deepAccess, isArray, - getBidIdParameter, deepSetValue, isEmpty, _each, - convertTypes, parseUrl, mergeDeep, buildUrl, _map, logError, isFn, - isPlainObject, + isPlainObject, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; // Maintainer: mediapsr@epsilon.com diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js index 9e237ef2558..e076fb4b0bb 100755 --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -1,8 +1,8 @@ - import * as utils from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'cpmstar'; @@ -49,13 +49,13 @@ export const spec = { var bidRequest = validBidRequests[i]; var referer = bidderRequest.refererInfo.page ? bidderRequest.refererInfo.page : bidderRequest.refererInfo.domain; referer = encodeURIComponent(referer); - var e = utils.getBidIdParameter('endpoint', bidRequest.params); + var e = getBidIdParameter('endpoint', bidRequest.params); var ENDPOINT = e == 'dev' ? ENDPOINT_DEV : e == 'staging' ? ENDPOINT_STAGING : ENDPOINT_PRODUCTION; var mediaType = spec.getMediaType(bidRequest); var playerSize = spec.getPlayerSize(bidRequest); var videoArgs = '&fv=0' + (playerSize ? ('&w=' + playerSize[0] + '&h=' + playerSize[1]) : ''); var url = ENDPOINT + '?media=' + mediaType + (mediaType == VIDEO ? videoArgs : '') + - '&json=c_b&mv=1&poolid=' + utils.getBidIdParameter('placementId', bidRequest.params) + + '&json=c_b&mv=1&poolid=' + getBidIdParameter('placementId', bidRequest.params) + '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + '&requestid=' + bidRequest.bidId + '&referer=' + encodeURIComponent(referer); diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 8f7821173c1..a2a054d7659 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,4 +1,4 @@ -import {convertCamelToUnderscore, convertTypes, getBidRequest, logError} from '../src/utils.js'; +import {getBidRequest, logError} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {auctionManager} from '../src/auctionManager.js'; @@ -7,7 +7,9 @@ import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 9ff6b540467..2fbf3d9ab9e 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -16,6 +16,9 @@ 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: '; @@ -28,7 +31,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 139; +export const FAST_BID_VERSION_CURRENT = 144; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; @@ -122,7 +125,8 @@ export const spec = { return []; }, - /** f + /** + * f * @param {object} bid * @return {boolean} */ @@ -201,7 +205,7 @@ export const spec = { /** * @param {*} response * @param {ServerRequest} request - * @return {Bid[]} + * @return {Bid[] | {bids: Bid[], fledgeAuctionConfigs: object[]}} */ interpretResponse: (response, request) => { const body = response.body || response; @@ -215,6 +219,7 @@ export const spec = { } const bids = []; + const fledgeAuctionConfigs = []; if (body && body.slots && isArray(body.slots)) { body.slots.forEach(slot => { @@ -268,6 +273,51 @@ 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 (!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), + }, + }); + }); + } + + if (fledgeAuctionConfigs.length) { + return { + bids, + fledgeAuctionConfigs, + }; + } + return bids; }, /** @@ -503,6 +553,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (hasVideoMediaType(bidRequest)) { const video = { + context: bidRequest.mediaTypes.video.context, playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), mimes: bidRequest.mediaTypes.video.mimes, protocols: bidRequest.mediaTypes.video.protocols, @@ -513,7 +564,19 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { minduration: bidRequest.mediaTypes.video.minduration, playbackmethod: bidRequest.mediaTypes.video.playbackmethod, startdelay: bidRequest.mediaTypes.video.startdelay, - plcmt: bidRequest.mediaTypes.video.plcmt + plcmt: bidRequest.mediaTypes.video.plcmt, + w: bidRequest.mediaTypes.video.w, + h: bidRequest.mediaTypes.video.h, + linearity: bidRequest.mediaTypes.video.linearity, + skipmin: bidRequest.mediaTypes.video.skipmin, + skipafter: bidRequest.mediaTypes.video.skipafter, + minbitrate: bidRequest.mediaTypes.video.minbitrate, + maxbitrate: bidRequest.mediaTypes.video.maxbitrate, + delivery: bidRequest.mediaTypes.video.delivery, + pos: bidRequest.mediaTypes.video.pos, + playbackend: bidRequest.mediaTypes.video.playbackend, + adPodDurationSec: bidRequest.mediaTypes.video.adPodDurationSec, + durationRangeSec: bidRequest.mediaTypes.video.durationRangeSec, }; const paramsVideo = bidRequest.params.video; if (paramsVideo !== undefined) { @@ -529,6 +592,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { enrichSlotWithFloors(slot, bidRequest); + if (!bidderRequest.fledgeEnabled && slot.ext?.ae) { + delete slot.ext.ae; + } + return slot; }), }; @@ -547,6 +614,8 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { }; request.user = bidderRequest.ortb2?.user || {}; request.site = bidderRequest.ortb2?.site || {}; + request.app = bidderRequest.ortb2?.app || {}; + request.device = bidderRequest.ortb2?.device || {}; if (bidderRequest && bidderRequest.ceh) { request.user.ceh = bidderRequest.ceh; } @@ -621,17 +690,7 @@ function hasValidVideoMediaType(bidRequest) { } }); - if (isValid) { - const videoPlacement = bidRequest.mediaTypes.video.placement || bidRequest.params.video.placement; - // We do not support long form for now, also we have to check that context & placement are consistent - if (bidRequest.mediaTypes.video.context == 'instream' && videoPlacement === 1) { - return true; - } else if (bidRequest.mediaTypes.video.context == 'outstream' && videoPlacement !== 1) { - return true; - } - } - - return false; + return isValid; } /** diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index ee343d9b16a..6a09ce2c973 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -22,6 +22,9 @@ const bundleStorageKey = 'cto_bundle'; const dnaBundleStorageKey = 'cto_dna_bundle'; const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000; +const STORAGE_TYPE_LOCALSTORAGE = 'html5'; +const STORAGE_TYPE_COOKIES = 'cookie'; + const pastDateString = new Date(0).toString(); const expirationString = new Date(timestamp() + cookiesMaxAge).toString(); @@ -32,14 +35,26 @@ function extractProtocolHost(url, returnOnlyHost = false) { : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; } -function getFromAllStorages(key) { +function getFromStorage(submoduleConfig, key) { + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + return storage.getDataFromLocalStorage(key); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + return storage.getCookie(key); + } + return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } -function saveOnAllStorages(key, value, hostname) { +function saveOnStorage(submoduleConfig, key, value, hostname) { if (key && value) { - storage.setDataInLocalStorage(key, value); - setCookieOnAllDomains(key, value, expirationString, hostname, true); + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } else { + storage.setDataInLocalStorage(key, value); + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } } } @@ -70,11 +85,11 @@ function deleteFromAllStorages(key, hostname) { storage.removeDataFromLocalStorage(key); } -function getCriteoDataFromAllStorages() { +function getCriteoDataFromStorage(submoduleConfig) { return { - bundle: getFromAllStorages(bundleStorageKey), - dnaBundle: getFromAllStorages(dnaBundleStorageKey), - bidId: getFromAllStorages(bididStorageKey), + bundle: getFromStorage(submoduleConfig, bundleStorageKey), + dnaBundle: getFromStorage(submoduleConfig, dnaBundleStorageKey), + bidId: getFromStorage(submoduleConfig, bididStorageKey), } } @@ -108,7 +123,7 @@ function buildCriteoUsersyncUrl(topUrl, domain, bundle, dnaBundle, areCookiesWri return url; } -function callSyncPixel(domain, pixel) { +function callSyncPixel(submoduleConfig, domain, pixel) { if (pixel.writeBundleInStorage && pixel.bundlePropertyName && pixel.storageKeyName) { ajax( pixel.pixelUrl, @@ -117,7 +132,7 @@ function callSyncPixel(domain, pixel) { if (response) { const jsonResponse = JSON.parse(response); if (jsonResponse && jsonResponse[pixel.bundlePropertyName]) { - saveOnAllStorages(pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); + saveOnStorage(submoduleConfig, pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); } } }, @@ -133,9 +148,9 @@ function callSyncPixel(domain, pixel) { } } -function callCriteoUserSync(parsedCriteoData, callback) { - const cw = storage.cookiesAreEnabled(); - const lsw = storage.localStorageIsEnabled(); +function callCriteoUserSync(submoduleConfig, parsedCriteoData, callback) { + const cw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) && storage.cookiesAreEnabled(); + const lsw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) && storage.localStorageIsEnabled(); const topUrl = extractProtocolHost(getRefererInfo().page); // TODO: should domain really be extracted from the current frame? const domain = extractProtocolHost(document.location.href, true); @@ -156,18 +171,18 @@ function callCriteoUserSync(parsedCriteoData, callback) { const jsonResponse = JSON.parse(response); if (jsonResponse.pixels) { - jsonResponse.pixels.forEach(pixel => callSyncPixel(domain, pixel)); + jsonResponse.pixels.forEach(pixel => callSyncPixel(submoduleConfig, domain, pixel)); } if (jsonResponse.acwsUrl) { const urlsToCall = typeof jsonResponse.acwsUrl === 'string' ? [jsonResponse.acwsUrl] : jsonResponse.acwsUrl; urlsToCall.forEach(url => triggerPixel(url)); } else if (jsonResponse.bundle) { - saveOnAllStorages(bundleStorageKey, jsonResponse.bundle, domain); + saveOnStorage(submoduleConfig, bundleStorageKey, jsonResponse.bundle, domain); } if (jsonResponse.bidId) { - saveOnAllStorages(bididStorageKey, jsonResponse.bidId, domain); + saveOnStorage(submoduleConfig, bididStorageKey, jsonResponse.bidId, domain); const criteoId = { criteoId: jsonResponse.bidId }; callback(criteoId); } else { @@ -207,10 +222,10 @@ export const criteoIdSubmodule = { * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId() { - let localData = getCriteoDataFromAllStorages(); + getId(submoduleConfig) { + let localData = getCriteoDataFromStorage(submoduleConfig); - const result = (callback) => callCriteoUserSync(localData, callback); + const result = (callback) => callCriteoUserSync(submoduleConfig, localData, callback); return { id: localData.bidId ? { criteoId: localData.bidId } : undefined, diff --git a/modules/currency.js b/modules/currency.js index 3da0cfe73e8..eaed4c50df2 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -7,29 +7,24 @@ import {getHook} from '../src/hook.js'; import {defer} from '../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; +import {on as onEvent, off as offEvent} from '../src/events.js'; const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$'; const CURRENCY_RATE_PRECISION = 4; -var bidResponseQueue = []; -var conversionCache = {}; -var currencyRatesLoaded = false; -var needToCallForCurrencyFile = true; -var adServerCurrency = 'USD'; +let ratesURL; +let bidResponseQueue = []; +let conversionCache = {}; +let currencyRatesLoaded = false; +let needToCallForCurrencyFile = true; +let adServerCurrency = 'USD'; export var currencySupportEnabled = false; export var currencyRates = {}; -var bidderCurrencyDefault = {}; -var defaultRates; +let bidderCurrencyDefault = {}; +let defaultRates; -export const ready = (() => { - let ctl; - function reset() { - ctl = defer(); - } - reset(); - return {done: () => ctl.resolve(), reset, promise: () => ctl.promise} -})(); +export let responseReady = defer(); /** * Configuration function for currency @@ -64,7 +59,7 @@ export const ready = (() => { * there is an error loading the config.conversionRateFile. */ export function setConfig(config) { - let url = DEFAULT_CURRENCY_RATE_URL; + ratesURL = DEFAULT_CURRENCY_RATE_URL; if (typeof config.rates === 'object') { currencyRates.conversions = config.rates; @@ -86,14 +81,14 @@ export function setConfig(config) { adServerCurrency = config.adServerCurrency; if (config.conversionRateFile) { logInfo('currency using override conversionRateFile:', config.conversionRateFile); - url = config.conversionRateFile; + ratesURL = config.conversionRateFile; } // see if the url contains a date macro // this is a workaround to the fact that jsdelivr doesn't currently support setting a 24-hour HTTP cache header // So this is an approach to let the browser cache a copy of the file each day // We should remove the macro once the CDN support a day-level HTTP cache setting - const macroLocation = url.indexOf('$$TODAY$$'); + const macroLocation = ratesURL.indexOf('$$TODAY$$'); if (macroLocation !== -1) { // get the date to resolve the macro const d = new Date(); @@ -104,10 +99,10 @@ export function setConfig(config) { const todaysDate = `${d.getFullYear()}${month}${day}`; // replace $$TODAY$$ with todaysDate - url = `${url.substring(0, macroLocation)}${todaysDate}${url.substring(macroLocation + 9, url.length)}`; + ratesURL = `${ratesURL.substring(0, macroLocation)}${todaysDate}${ratesURL.substring(macroLocation + 9, ratesURL.length)}`; } - initCurrency(url); + initCurrency(); } else { // currency support is disabled, setting defaults logInfo('disabling currency support'); @@ -128,20 +123,11 @@ function errorSettingsRates(msg) { } } -function initCurrency(url) { - conversionCache = {}; - currencySupportEnabled = true; - - logInfo('Installing addBidResponse decorator for currency module', arguments); - - // Adding conversion function to prebid global for external module and on page use - getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); - getHook('addBidResponse').before(addBidResponseHook, 100); - - // call for the file if we haven't already +function loadRates() { if (needToCallForCurrencyFile) { needToCallForCurrencyFile = false; - ajax(url, + currencyRatesLoaded = false; + ajax(ratesURL, { success: function (response) { try { @@ -150,26 +136,45 @@ function initCurrency(url) { conversionCache = {}; currencyRatesLoaded = true; processBidResponseQueue(); - ready.done(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } }, error: function (...args) { errorSettingsRates(...args); - ready.done(); + currencyRatesLoaded = true; + processBidResponseQueue(); + needToCallForCurrencyFile = true; } } ); } else { - ready.done(); + processBidResponseQueue(); } } +function initCurrency() { + conversionCache = {}; + currencySupportEnabled = true; + + logInfo('Installing addBidResponse decorator for currency module', arguments); + + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); + getHook('addBidResponse').before(addBidResponseHook, 100); + getHook('responsesReady').before(responsesReadyHook); + onEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + onEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); + loadRates(); +} + function resetCurrency() { logInfo('Uninstalling addBidResponse decorator for currency module', arguments); getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); + offEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + offEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); delete getGlobal().convertCurrency; adServerCurrency = 'USD'; @@ -179,6 +184,11 @@ function resetCurrency() { needToCallForCurrencyFile = true; currencyRates = {}; bidderCurrencyDefault = {}; + responseReady = defer(); +} + +function responsesReadyHook(next, ready) { + next(ready.then(() => responseReady.promise)); } export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid, reject) { @@ -211,24 +221,25 @@ export const addBidResponseHook = timedBidResponseHook('currency', function addB if (bid.currency === adServerCurrency) { return fn.call(this, adUnitCode, bid, reject); } - - bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid, reject])); + bidResponseQueue.push([fn, this, adUnitCode, bid, reject]); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); - } else { - fn.untimed.bail(ready.promise()); } }); -function processBidResponseQueue() { - while (bidResponseQueue.length > 0) { - (bidResponseQueue.shift())(); - } +function rejectOnAuctionTimeout({auctionId}) { + bidResponseQueue = bidResponseQueue.filter(([fn, ctx, adUnitCode, bid, reject]) => { + if (bid.auctionId === auctionId) { + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY) + } else { + return true; + } + }); } -function wrapFunction(fn, context, params) { - return function() { - let bid = params[1]; +function processBidResponseQueue() { + while (bidResponseQueue.length > 0) { + const [fn, ctx, adUnitCode, bid, reject] = bidResponseQueue.shift(); if (bid !== undefined && 'currency' in bid && 'cpm' in bid) { let fromCurrency = bid.currency; try { @@ -239,12 +250,13 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('getCurrencyConversion threw error: ', e); - params[2](CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); - return; + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + continue; } } - return fn.apply(context, params); - }; + fn.call(ctx, adUnitCode, bid, reject); + } + responseReady.resolve(); } function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) { diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 11d3ebb1589..395706994fe 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,10 +1,11 @@ -import {deepAccess, getAdUnitSizes, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; +import {deepAccess, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; export const storage = getStorageManager({bidderCode: 'datablocks'}); diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index 2cf28c36330..127e7893ec5 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -1,4 +1,12 @@ -import { deepAccess, getBidIdParameter, isArray, getUniqueIdentifierStr, contains, isFn, isPlainObject } from '../src/utils.js'; +import { + deepAccess, + isArray, + getUniqueIdentifierStr, + contains, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; diff --git a/modules/dchain.js b/modules/dchain.js index daf97a7551f..7f84282b81e 100644 --- a/modules/dchain.js +++ b/modules/dchain.js @@ -1,7 +1,7 @@ import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; -import {_each, deepAccess, deepClone, hasOwn, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; +import {_each, deepAccess, deepClone, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; const shouldBeAString = ' should be a string'; @@ -49,7 +49,7 @@ export function checkDchainSyntax(bid, mode) { appendFailMsg(`dchain.ver` + shouldBeAString); } - if (hasOwn(dchainObj, 'ext')) { + if (dchainObj.hasOwnProperty('ext')) { if (!isPlainObject(dchainObj.ext)) { appendFailMsg(`dchain.ext` + shouldBeAnObject); } diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index e062686b320..7c24cd6a8f6 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -2,6 +2,7 @@ import { generateUUID, deepSetValue, deepAccess, isArray, isInteger, logError, l import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'deepintent'; +const GVL_ID = 541; const BIDDER_ENDPOINT = 'https://prebid.deepintent.com/prebid'; const USER_SYNC_URL = 'https://cdn.deepintent.com/syncpixel.html'; const DI_M_V = '1.0.0'; @@ -32,6 +33,7 @@ export const ORTB_VIDEO_PARAMS = { }; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: [], diff --git a/modules/deltaprojectsBidAdapter.js b/modules/deltaprojectsBidAdapter.js index c66e381b8f1..870378a13dd 100644 --- a/modules/deltaprojectsBidAdapter.js +++ b/modules/deltaprojectsBidAdapter.js @@ -16,7 +16,7 @@ export const BIDDER_CODE = 'deltaprojects'; export const BIDDER_ENDPOINT_URL = 'https://d5p.de17a.com/dogfight/prebid'; export const USERSYNC_URL = 'https://userservice.de17a.com/getuid/prebid'; -/** -- isBidRequestValid --**/ +/** -- isBidRequestValid -- */ function isBidRequestValid(bid) { if (!bid) return false; @@ -32,9 +32,9 @@ function isBidRequestValid(bid) { return true; } -/** -- Build requests --**/ +/** -- Build requests -- */ function buildRequests(validBidRequests, bidderRequest) { - /** == shared ==**/ + /** == shared == */ // -- build id const id = bidderRequest.bidderRequestId; @@ -146,7 +146,7 @@ function buildImpressionBanner(bid, bannerMediaType) { }; } -/** -- Interpret response --**/ +/** -- Interpret response -- */ function interpretResponse(serverResponse) { if (!serverResponse.body) { logWarn('Response body is invalid, return !!'); @@ -189,7 +189,7 @@ function interpretResponse(serverResponse) { return bidResponses; } -/** -- On Bid Won -- **/ +/** -- On Bid Won -- */ function onBidWon(bid) { let cpm = bid.cpm; if (bid.currency && bid.currency !== bid.originalCurrency && typeof bid.getCpmInNewCurrency === 'function') { @@ -200,7 +200,7 @@ function onBidWon(bid) { bid.ad = bid.ad.replace(wonPriceMacroPatten, wonPrice); } -/** -- Get user syncs --**/ +/** -- Get user syncs -- */ function getUserSyncs(syncOptions, serverResponses, gdprConsent) { const syncs = [] @@ -223,7 +223,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent) { return syncs; } -/** -- Get bid floor --**/ +/** -- Get bid floor -- */ export function getBidFloor(bid, mediaType, size, currency) { if (isFn(bid.getFloor)) { const bidFloorCurrency = currency || 'USD'; @@ -234,7 +234,7 @@ export function getBidFloor(bid, mediaType, size, currency) { } } -/** -- Helper methods --**/ +/** -- Helper methods -- */ function setOnAny(collection, key) { for (let i = 0, result; i < collection.length; i++) { result = deepAccess(collection[i], key); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 3394fd8b3f4..a3e26dc7202 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -249,7 +249,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { */ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); - if (descriptionUrl) { components.search.description_url = descriptionUrl; } + if (descriptionUrl) { + components.search.description_url = descriptionUrl; + } components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); @@ -264,7 +266,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { * @return {string | undefined} The encoded vast url if it exists, or undefined */ function getDescriptionUrl(bid, components, prop) { - return deepAccess(components, `${prop}.description_url`) || dep.ri().page; + return deepAccess(components, `${prop}.description_url`) || encodeURIComponent(dep.ri().page); } /** diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index 99df3b18a39..76f5f04ac03 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -6,8 +6,7 @@ * @module modules/dgkeywordProvider * @requires module:modules/realTimeData */ - -import {logMessage, deepSetValue, logError, logInfo, mergeDeep} from '../src/utils.js'; +import { logMessage, deepSetValue, logError, logInfo, isStr, isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getGlobal } from '../src/prebidGlobal.js'; @@ -57,20 +56,12 @@ export function getDgKeywordsAndSet(reqBidsConfigObj, callback, moduleConfig, us if (Object.keys(keywords).length > 0) { const targetBidKeys = {}; for (let bid of setKeywordTargetBidders) { - // set keywords to params - bid.params.keywords = keywords; + // set keywords to ortb2Imp + deepSetValue(bid, 'ortb2Imp.ext.data.keywords', convertKeywordsToString(keywords)); if (!targetBidKeys[bid.bidder]) { targetBidKeys[bid.bidder] = true; } } - - if (!reqBidsConfigObj._ignoreSetOrtb2) { - // set keywrods to ortb2 - let addOrtb2 = {}; - deepSetValue(addOrtb2, 'site.keywords', keywords); - deepSetValue(addOrtb2, 'user.keywords', keywords); - mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, Object.fromEntries(Object.keys(targetBidKeys).map(bidder => [bidder, addOrtb2]))); - } } } isFinish = true; @@ -156,4 +147,37 @@ function init(moduleConfig) { function registerSubModule() { submodule('realTimeData', dgkeywordSubmodule); } + +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +export function convertKeywordsToString(keywords) { + let result = ''; + Object.keys(keywords).forEach(key => { + // if 'text' or '' + if (isStr(keywords[key])) { + if (keywords[key] !== '') { + result += `${key}=${keywords[key]},` + } else { + result += `${key},`; + } + } else if (isArray(keywords[key])) { + let isValSet = false + keywords[key].forEach(val => { + if (isStr(val) && val) { + result += `${key}=${val},` + isValSet = true + } + }); + if (!isValSet) { + result += `${key},` + } + } else { + result += `${key},` + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + registerSubModule(); diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 7ad75f64215..551c78c1e24 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -6,7 +6,7 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; const BIDDER_CODE = 'discovery'; const ENDPOINT_URL = 'https://rtb-jp.mediago.io/api/bid?tn='; const TIME_TO_LIVE = 500; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; const MEDIATYPE = [BANNER, NATIVE]; @@ -14,6 +14,9 @@ const MEDIATYPE = [BANNER, NATIVE]; /* ----- _ss_pp_id:start ------ */ const COOKIE_KEY_SSPPID = '_ss_pp_id'; 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'; const NATIVERET = { id: 'id', @@ -55,24 +58,81 @@ const NATIVERET = { }; /** - * čŽˇå–į”¨æˆˇid + * 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) || ''; + } +} + +/** + * get page description + * @returns {string} + */ +export function getPageDescription(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="description"]') || + win.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +/** + * get page keywords + * @returns {string} + */ +export function getPageKeywords(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); + } + + return (element && element.content) || ''; +} + +/** + * get connection downlink + * @returns {number} + */ +export function getConnectionDownLink(win = window) { + const nav = win.navigator || {}; + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; +} + +/** + * get pmg uid + * čŽˇå–åšļį”Ÿæˆį”¨æˆˇįš„id * @return {string} */ -const getUserID = () => { - let idd = storage.getCookie(COOKIE_KEY_SSPPID); - let idm = storage.getCookie(COOKIE_KEY_MGUID); - - if (idd && !idm) { - idm = idd; - } else if (idm && !idd) { - idd = idm; - } else if (!idd && !idm) { - const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid); - storage.setCookie(COOKIE_KEY_SSPPID, uuid); - return uuid; +export const getPmgUID = () => { + if (!storage.cookiesAreEnabled()) return; + + let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID); + if (!pmgUid) { + pmgUid = utils.generateUUID(); + const date = new Date(); + date.setTime(date.getTime() + COOKIE_RETENTION_TIME); + try { + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, date.toUTCString()); + } catch (e) {} } - return idd; + return pmgUid; }; /* ----- _ss_pp_id:end ------ */ @@ -211,6 +271,64 @@ const popInAdSize = [ { w: 336, h: 280 }, ]; +/** + * get screen size + * + * @returns {Array} eg: "['widthxheight']" + */ +function getScreenSize() { + return utils.parseSizesInput([window.screen.width, window.screen.height]); +} + +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + +/** + * format imp ad test ext params + * + * @param validBidRequest sigleBidRequest + * @param bidderRequest + */ +function addImpExtParams(bidRequest = {}, bidderRequest = {}) { + const { deepAccess } = utils; + const { params = {}, adUnitCode, bidId } = bidRequest; + const ext = { + bidId: bidId || '', + adUnitCode: adUnitCode || '', + token: params.token || '', + siteId: params.siteId || '', + zoneId: params.zoneId || '', + publisher: params.publisher || '', + p_pos: params.position || '', + screenSize: getScreenSize(), + referrer: getReferrer(bidRequest, bidderRequest), + stack: deepAccess(bidRequest, 'refererInfo.stack', []), + b_pos: deepAccess(bidRequest, 'mediaTypes.banner.pos', '', ''), + ortbUser: deepAccess(bidRequest, 'ortb2.user', {}, {}), + ortbSite: deepAccess(bidRequest, 'ortb2.site', {}, {}), + tid: deepAccess(bidRequest, 'ortb2Imp.ext.tid', '', ''), + browsiViewability: deepAccess(bidRequest, 'ortb2Imp.ext.data.browsi.browsiViewability', '', ''), + adserverName: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name', '', ''), + adslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + keywords: deepAccess(bidRequest, 'ortb2Imp.ext.data.keywords', '', ''), + gpid: deepAccess(bidRequest, 'ortb2Imp.ext.gpid', '', ''), + pbadslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot', '', ''), + }; + return ext; +} + /** * get aditem setting * @param {Array} validBidRequests an an array of bids @@ -261,6 +379,11 @@ function getItems(validBidRequests, bidderRequest) { tagid: req.params && req.params.tagid }; } + + try { + ret.ext = addImpExtParams(req, bidderRequest); + } catch (e) {} + itemMaps[id] = { req, ret, @@ -298,6 +421,10 @@ function getParam(validBidRequests, bidderRequest) { const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const firstPartyData = bidderRequest.ortb2; + const topWindow = window.top; + const title = getPageTitle(); + const desc = getPageDescription(); + const keywords = getPageKeywords(); if (items && items.length) { let c = { @@ -318,12 +445,24 @@ function getParam(validBidRequests, bidderRequest) { ext: { eids, firstPartyData, + ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined, + pmguid: getPmgUID(), + page: { + title: title ? title.slice(0, 100) : undefined, + desc: desc ? desc.slice(0, 300) : undefined, + keywords: keywords ? keywords.slice(0, 100) : undefined, + hLen: topWindow.history?.length || undefined, + }, + device: { + nbw: getConnectionDownLink(), + hc: topWindow.navigator?.hardwareConcurrency || undefined, + dm: topWindow.navigator?.deviceMemory || undefined, + } }, user: { - buyeruid: getUserID(), + buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, id: sharedid || pubcid, }, - eids, tmax: timeout, site: { name: domain, @@ -485,6 +624,31 @@ export const spec = { return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + } + }, + /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index 3d34f2c8b26..3cdfd3a77cd 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import {getWindowFromDocument, logWarn} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; @@ -156,7 +156,7 @@ function newRenderer(bid) { function webisRender(bid, doc) { bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.webis.init(bid.adData, bid.adUnitCode, bid.params); }) } diff --git a/modules/docereeAdManagerBidAdapter.js b/modules/docereeAdManagerBidAdapter.js new file mode 100644 index 00000000000..d3765f5a130 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.js @@ -0,0 +1,128 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'docereeadmanager'; +const END_POINT = 'https://dai.doceree.com/drs/quest'; + +export const spec = { + code: BIDDER_CODE, + url: '', + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + const { placementId } = bid.params; + return !!placementId; + }, + isGdprConsentPresent: (bid) => { + const { gdpr, gdprconsent } = bid.params; + if (gdpr == '1') { + return !!gdprconsent; + } + return true; + }, + buildRequests: (validBidRequests) => { + const serverRequests = []; + const { data } = config.getConfig('docereeadmanager.user') || {}; + + validBidRequests.forEach(function (validBidRequest) { + const payload = getPayload(validBidRequest, data); + + if (!payload) { + return; + } + + serverRequests.push({ + method: 'POST', + url: END_POINT, + data: JSON.stringify(payload.data), + options: { + contentType: 'application/json', + withCredentials: true, + }, + }); + }); + + return serverRequests; + }, + interpretResponse: (serverResponse) => { + const responseJson = serverResponse ? serverResponse.body : {}; + const bidResponse = { + ad: responseJson.ad, + width: Number(responseJson.width), + height: Number(responseJson.height), + requestId: responseJson.requestId, + netRevenue: true, + ttl: 30, + cpm: responseJson.cpm, + currency: responseJson.currency, + mediaType: BANNER, + creativeId: responseJson.creativeId, + meta: { + advertiserDomains: + Array.isArray(responseJson.meta.advertiserDomains) && + responseJson.meta.advertiserDomains.length > 0 + ? responseJson.meta.advertiserDomains + : [], + }, + }; + + return [bidResponse]; + }, +}; + +function getPayload(bid, userData) { + if (!userData || !bid) { + return false; + } + + const { bidId, params } = bid; + const { placementId } = params; + const { + userid, + email, + firstname, + lastname, + specialization, + hcpid, + gender, + city, + state, + zipcode, + hashedNPI, + hashedhcpid, + hashedemail, + hashedmobile, + country, + organization, + dob, + } = userData; + + const data = { + userid: userid || '', + email: email || '', + firstname: firstname || '', + lastname: lastname || '', + specialization: specialization || '', + hcpid: hcpid || '', + gender: gender || '', + city: city || '', + state: state || '', + zipcode: zipcode || '', + hashedNPI: hashedNPI || '', + pb: 1, + adunit: placementId || '', + requestId: bidId || '', + hashedhcpid: hashedhcpid || '', + hashedemail: hashedemail || '', + hashedmobile: hashedmobile || '', + country: country || '', + organization: organization || '', + dob: dob || '', + userconsent: 1, + }; + return { + data, + }; +} + +registerBidder(spec); diff --git a/modules/docereeAdManagerBidAdapter.md b/modules/docereeAdManagerBidAdapter.md new file mode 100644 index 00000000000..bedbf57b179 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Doceree AdManager Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech.stack@doceree.com +``` + + + +Connects to Doceree demand source to fetch bids. +Please use `docereeadmanager` as the bidder code. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'DOC-397-1', + sizes: [ + [300, 250] + ], + bids: [ + { + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', //required + publisherUrl: document.URL || window.location.href, //optional + gdpr: '1', //optional + gdprconsent:'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', //optional + } + } + ] + } +]; +``` + +```javascript +pbjs.setBidderConfig({ + bidders: ['docereeadmanager'], + config: { + docereeadmanager: { + user: { + data: { + email: 'XXX.XXX@GMAIL.COM', + firstname: 'DR. XXX', + lastname: 'XXX', + mobile: '981234XXXX', + specialization: 'Internal Medicine', + organization: 'Max Lifecare', + hcpid: '199291XXXX', + dob: '1987-08-27', + gender: 'Female', + city: 'Oildale', + state: 'California', + country: 'California', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '7d26d8ca-233a-46c2-9d36-7c5d261e151d', + zipcode: '', + userconsent: '1', + }, + }, + }, + }, +}); +``` diff --git a/modules/docereeBidAdapter.js b/modules/docereeBidAdapter.js index 524f464cee3..2731e1ff397 100644 --- a/modules/docereeBidAdapter.js +++ b/modules/docereeBidAdapter.js @@ -1,9 +1,11 @@ -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel } from '../src/utils.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'doceree'; const END_POINT = 'https://bidder.doceree.com' +const TRACKING_END_POINT = 'https://tracking.doceree.com' export const spec = { code: BIDDER_CODE, @@ -69,6 +71,33 @@ export const spec = { } }; return [bidResponse]; + }, + onTimeout: function(timeoutData) { + if (timeoutData == null || !timeoutData.length) { + return; + } + timeoutData.forEach(td => { + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + bidId: td.bidId, + timeout: td.timeout, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbTimeout?adp=prebidjs&data=' + encodedBuf); + }) + }, + onBidWon: function (bidWon) { + if (bidWon == null) { + return; + } + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + requestId: bidWon.requestId, + cpm: bidWon.cpm, + adId: bidWon.adId, + currency: bidWon.currency, + netRevenue: bidWon.netRevenue, + status: bidWon.status, + hb_pb: bidWon.adserverTargeting && bidWon.adserverTargeting.hb_pb, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbBidWon?adp=prebidjs&data=' + encodedBuf); } }; diff --git a/modules/dsp_genieeBidAdapter.js b/modules/dsp_genieeBidAdapter.js new file mode 100644 index 00000000000..17865e6793a --- /dev/null +++ b/modules/dsp_genieeBidAdapter.js @@ -0,0 +1,123 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { deepAccess, deepSetValue } from '../src/utils.js'; +import { config } from '../src/config.js'; +const BIDDER_CODE = 'dsp_geniee'; +const ENDPOINT_URL = 'https://rt.gsspat.jp/prebid_auction'; +const ENDPOINT_URL_UNCOMFORTABLE = 'https://rt.gsspat.jp/prebid_uncomfortable'; +const ENDPOINT_USERSYNC = 'https://rt.gsspat.jp/prebid_cs'; +const VALID_CURRENCIES = ['USD', 'JPY']; +const converter = ortbConverter({ + context: { ttl: 300, netRevenue: true }, + // set optional parameters + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext', bidRequest.params); + return imp; + } +}); + +function USPConsent(consent) { + return typeof consent === 'string' && consent[0] === '1' && consent.toUpperCase()[2] === 'Y'; +} + +function invalidCurrency(currency) { + return typeof currency === 'string' && VALID_CURRENCIES.indexOf(currency.toUpperCase()) === -1; +} + +function hasTest(imp) { + if (typeof imp !== 'object') { + return false; + } + for (let i = 0; i < imp.length; i++) { + if (deepAccess(imp[i], 'ext.test') === 1) { + return true; + } + } + return false; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} - The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (_) { + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - the master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || // gdpr + USPConsent(bidderRequest.uspConsent) || // usp + config.getConfig('coppa') || // coppa + invalidCurrency(config.getConfig('currency.adServerCurrency')) // currency validation + ) { + return { + method: 'GET', + url: ENDPOINT_URL_UNCOMFORTABLE + }; + } + + const payload = converter.toORTB({ validBidRequests, bidderRequest }); + + if (hasTest(deepAccess(payload, 'imp'))) { + deepSetValue(payload, 'test', 1); + } + + deepSetValue(payload, 'at', 1); // first price auction only + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest - the master bidRequest object + * @return {bids} - An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse.body) { // empty response (no bids) + return []; + } + const bids = converter.fromORTB({ response: serverResponse.body, request: bidRequest.data }).bids; + return bids; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + // gdpr & usp + if (deepAccess(gdprConsent, 'gdprApplies') || USPConsent(uspConsent)) { + return syncs; + } + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: ENDPOINT_USERSYNC + }); + } + return syncs; + } +}; +registerBidder(spec); diff --git a/modules/dsp_genieeBidAdapter.md b/modules/dsp_genieeBidAdapter.md new file mode 100644 index 00000000000..d51d66884af --- /dev/null +++ b/modules/dsp_genieeBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +```markdown +Module Name: Geniee Bid Adapter +Module Type: Bidder Adapter +Maintainer: dsp_back@geniee.co.jp +``` + +# Description +This is [Geniee](https://geniee.co.jp) Bidder Adapter for Prebid.js. + +Please contact us before using the adapter. + +We will provide ads when satisfy the following conditions: + +- There are a certain number bid requests by zone +- The request is a Banner ad +- Payment is possible in Japanese yen or US dollars +- The request is not for GDPR or COPPA users + +Thus, even if the following test, it will be no bids if the request does not reach a certain requests. + +# Test AdUnits +```javascript +var adUnits={ + code: 'geniee-test-ad', + bids: [{ + bidder: 'dsp_geniee', + params: { + test: 1, + } + }], + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } +}; +``` diff --git a/modules/dxkultureBidAdapter.js b/modules/dxkultureBidAdapter.js new file mode 100644 index 00000000000..282b54e0823 --- /dev/null +++ b/modules/dxkultureBidAdapter.js @@ -0,0 +1,335 @@ +import { + logInfo, + logWarn, + logError, + logMessage, + deepAccess, + deepSetValue, + mergeDeep +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'dxkulture'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_OUTSTREAM_RENDERER_URL = 'https://cdn.dxkulture.com/players/dxOutstreamPlayer.js'; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: '1.0.0', + } + }) + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(req, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return req; + }, + bidResponse(buildBidResponse, bid, context) { + let resMediaType; + const {bidRequest} = context; + + if (bid.adm?.trim().startsWith(' { + const userSync = deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + let syncDetails = []; + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + syncDetails = syncDetails.concat(value.syncs); + } + }); + syncDetails.forEach(syncDetails => { + let queryParamStrings = []; + let syncUrl = syncDetails.url; + + if (syncDetails.type === 'iframe') { + if (gdprConsent) { + queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + syncUrl = `${syncDetails.url}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}` + } + + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncUrl + }); + }); + + if (syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type == 'iframe'); + } else if (syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type == 'image'); + } + } + }); + logInfo('dxkulture.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; + +function outstreamRenderer(bid) { + const rendererConfig = { + width: bid.width, + height: bid.height, + vastTimeout: 5000, + maxAllowedVastTagRedirects: 3, + allowVpaid: false, + autoPlay: true, + preload: true, + mute: false + } + + const renderer = Renderer.install({ + id: bid.adId, + url: DEFAULT_OUTSTREAM_RENDERER_URL, + config: rendererConfig, + loaded: false, + targetId: bid.adUnitCode, + adUnitCode: bid.adUnitCode + }); + + try { + renderer.setRender(function (bid) { + bid.renderer.push(() => { + const { id, config } = bid.renderer; + window.dxOutstreamPlayer(bid, id, config); + }); + }); + } catch (err) { + logWarn('dxkulture: Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +/* ======================================= + * Util Functions + *======================================= */ + +function hasBannerMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +function hasVideoMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.video'); +} + +function _validateParams(bidRequest) { + if (!bidRequest.params) { + return false; + } + + if (bidRequest.params.e2etest) { + return true; + } + + if (!bidRequest.params.publisherId) { + logError('dxkulture: Validation failed: publisherId not declared'); + return false; + } + + if (!bidRequest.params.placementId) { + logError('dxkulture: Validation failed: placementId not declared'); + return false; + } + + const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); + if (!mediaTypesExists) { + return false; + } + + return true; +} + +/** + * Validates banner bid request. If it is not banner media type returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateBanner(bidRequest) { + // If there's no banner no need to validate + if (!hasBannerMediaType(bidRequest)) { + return true; + } + const banner = deepAccess(bidRequest, 'mediaTypes.banner'); + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; +} + +/** + * Validates video bid request. If it is not video media type returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateVideo(bidRequest) { + // If there's no video no need to validate + if (!hasVideoMediaType(bidRequest)) { + return true; + } + + const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + const params = deepAccess(bidRequest, 'params', {}); + + if (params && params.e2etest) { + return true; + } + + const videoParams = { + ...videoPlacement, + ...videoBidderParams // Bidder Specific overrides + }; + + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + logError('dxkulture: Validation failed: mimes are invalid'); + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + logError('dxkulture: Validation failed: protocols are invalid'); + return false; + } + + if (!videoParams.context) { + logError('dxkulture: Validation failed: context id not declared'); + return false; + } + + if (videoParams.context !== 'instream') { + logError('dxkulture: Validation failed: only context instream is supported '); + return false; + } + + if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { + logError('dxkulture: Validation failed: player size not declared or is not in format [[w,h]]'); + return false; + } + + return true; +} + +registerBidder(spec); diff --git a/modules/kulturemediaBidAdapter.md b/modules/dxkultureBidAdapter.md similarity index 86% rename from modules/kulturemediaBidAdapter.md rename to modules/dxkultureBidAdapter.md index 0bd17e97982..e31794ef6c6 100644 --- a/modules/kulturemediaBidAdapter.md +++ b/modules/dxkultureBidAdapter.md @@ -1,15 +1,15 @@ # Overview ``` -Module Name: Kulture Media Bid Adapter +Module Name: DXKulture Bid Adapter Module Type: Bidder Adapter Maintainer: devops@kulture.media ``` # Description -Module that connects to Kulture Media's demand sources. -Kulture Media bid adapter supports Banner and Video. +Module that connects to DXKulture's demand sources. +DXKulture bid adapter supports Banner and Video. # Test Parameters @@ -26,10 +26,12 @@ var adUnits = [ } }, bids: [{ - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { placementId: 'test', publisherId: 'test', + bidfloor: 2.7, + bidfloorcur: 'USD' } }] } @@ -42,7 +44,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid - 'mimes', - 'minduration', - 'maxduration', -- 'placement', +- 'plcmt', - 'protocols', - 'startdelay', - 'skip', @@ -73,13 +75,13 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid delivery: [2], minduration: 10, maxduration: 30, - placement: 1, + plcmt: 1, playbackmethod: [1,5], } }, bids: [ { - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { bidfloor: 0.5, publisherId: '12345', @@ -105,7 +107,7 @@ var adUnits = [ } }, bids: [{ - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { e2etest: true } @@ -129,7 +131,7 @@ var adUnits = [ }, bids: [ { - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { e2etest: true } diff --git a/modules/dynamicAdBoostRtdProvider.js b/modules/dynamicAdBoostRtdProvider.js new file mode 100644 index 00000000000..fe08795f313 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.js @@ -0,0 +1,114 @@ +/** + * The {@link module:modules/realTimeData} module is required + * @module modules/dynamicAdBoost + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js' +import { loadExternalScript } from '../src/adloader.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { deepAccess, deepSetValue, isEmptyStr } from '../src/utils.js'; + +const MODULE_NAME = 'dynamicAdBoost'; +const SCRIPT_URL = 'https://adxbid.info'; +const CLIENT_SUPPORTS_IO = window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype && + 'intersectionRatio' in window.IntersectionObserverEntry.prototype; +// Options for the Intersection Observer +const dabOptions = { + threshold: 0.5 // Trigger callback when 50% of the element is visible +}; +let observer; +let dabStartDate; +let dabStartTime; + +// Array of div IDs to track +let dynamicAdBoostAdUnits = {}; + +function init(config, userConsent) { + dabStartDate = new Date(); + dabStartTime = dabStartDate.getTime(); + if (!CLIENT_SUPPORTS_IO) { + return false; + } + // Create an Intersection Observer instance + observer = new IntersectionObserver(dabHandleIntersection, dabOptions); + if (config.params.keyId) { + let keyId = config.params.keyId; + if (keyId && !isEmptyStr(keyId)) { + let dabDivIdsToTrack = config.params.adUnits; + let dabInterval = setInterval(function() { + // Observe each div by its ID + dabDivIdsToTrack.forEach(divId => { + let div = document.getElementById(divId); + if (div) { + observer.observe(div); + } + }); + + let dabDateNow = new Date(); + let dabTimeNow = dabDateNow.getTime(); + let dabElapsedSeconds = Math.floor((dabTimeNow - dabStartTime) / 1000); + let elapsedThreshold = 30; + if (config.params.threshold) { + elapsedThreshold = config.params.threshold; + } + if (dabElapsedSeconds >= elapsedThreshold) { + clearInterval(dabInterval); // Stop + loadLmScript(keyId); + } + }, 1000); + + return true; + } + } + return false; +} + +function loadLmScript(keyId) { + let viewableAdUnits = Object.keys(dynamicAdBoostAdUnits); + let viewableAdUnitsCSV = viewableAdUnits.join(','); + const scriptUrl = `${SCRIPT_URL}/${keyId}.js?viewableAdUnits=${viewableAdUnitsCSV}`; + loadExternalScript(scriptUrl, MODULE_NAME); + observer.disconnect(); +} + +function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + const reqAdUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (Array.isArray(reqAdUnits)) { + reqAdUnits.forEach(adunit => { + let gptCode = deepAccess(adunit, 'code'); + if (dynamicAdBoostAdUnits.hasOwnProperty(gptCode)) { + // AdUnits has reached target viewablity at some point + deepSetValue(adunit, `ortb2Imp.ext.data.${MODULE_NAME}.${gptCode}`, dynamicAdBoostAdUnits[gptCode]); + } + }); + } + callback(); +} + +let markViewed = (entry, observer) => { + return () => { + observer.unobserve(entry.target); + } +} + +// Callback function when an observed element becomes visible +function dabHandleIntersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting && entry.intersectionRatio > 0.5) { + dynamicAdBoostAdUnits[entry.target.id] = entry.intersectionRatio; + markViewed(entry, observer) + } + }); +} + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init, + getBidRequestData, + markViewed +}; + +submodule('realTimeData', subModuleObj); diff --git a/modules/dynamicAdBoostRtdProvider.md b/modules/dynamicAdBoostRtdProvider.md new file mode 100644 index 00000000000..93efe3b3f97 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: Dynamic Ad Boost +Module Type: Track when a adunit is viewable +Maintainer: info@luponmedia.com + +# Description + +Enhance your revenue with the cutting-edge DynamicAdBoost module! By seamlessly integrating the powerful LuponMedia technology, our module retrieves adunits viewability data, providing publishers with valuable insights to optimize their revenue streams. To unlock the full potential of this technology, we provide a customized LuponMedia module tailored to your specific site requirements. Boost your ad revenue and gain unprecedented visibility into your performance with our advanced solution. + +In order to utilize this module, it is essential to collaborate with [LuponMedia](https://www.luponmedia.com/) to create an account and obtain detailed guidelines on configuring your sites. Working hand in hand with LuponMedia will ensure a smooth integration process, enabling you to fully leverage the capabilities of this module on your website. Take the first step towards optimizing your ad revenue and enhancing your site's performance by partnering with LuponMedia for a seamless experience. +Contact info@luponmedia.com for information. + +## Building Prebid with Real-time Data Support + +First, make sure to add the Dynamic AdBoost submodule to your Prebid.js package with: + +`gulp build --modules=rtdModule,dynamicAdBoostRtdProvider` + +The following configuration parameters are available: + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 2000, + dataProviders: [ + { + name: "dynamicAdBoost", + params: { + keyId: "[PROVIDED_KEY]", // Your provided Dynamic AdBoost keyId + adUnits: ["allowedAdUnit1", "allowedAdUnit2"], + threshold: 35 // optional + } + } + ] + } + ... +} +``` diff --git a/modules/ebdrBidAdapter.js b/modules/ebdrBidAdapter.js index a03a1ec12ca..e830f8a94f7 100644 --- a/modules/ebdrBidAdapter.js +++ b/modules/ebdrBidAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, logInfo} from '../src/utils.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'ebdr'; diff --git a/modules/edge226BidAdapter.js b/modules/edge226BidAdapter.js new file mode 100644 index 00000000000..6d1e2466abe --- /dev/null +++ b/modules/edge226BidAdapter.js @@ -0,0 +1,188 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'edge226'; +const AD_URL = 'https://ssp.dauup.com/pbjs'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + } +}; + +registerBidder(spec); diff --git a/modules/edge226BidAdapter.md b/modules/edge226BidAdapter.md new file mode 100644 index 00000000000..b38ff67065f --- /dev/null +++ b/modules/edge226BidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Edge226 Bidder Adapter +Module Type: Edge226 Bidder Adapter +Maintainer: audit@edge226.com +``` + +# Description + +Connects to Edge226 exchange for bids. +Edge226 bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 2216ab329b0..d57804c04e6 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,8 +1,9 @@ -import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined, isSlotMatchingAdUnitCode} from '../src/utils.js'; +import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'eplanning'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -267,6 +268,21 @@ function cleanName(name) { return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)|:/g, '_').replace(/^_+|_+$/g, ''); } +function getFloorStr(bid) { + if (typeof bid.getFloor === 'function') { + let bidFloor = bid.getFloor({ + currency: DOLLAR_CODE, + mediaType: '*', + size: '*' + }); + + if (bidFloor.floor) { + return '|' + encodeURIComponent(bidFloor.floor); + } + } + return ''; +} + function getSpaces(bidRequests, ml) { let impType = bidRequests.reduce((previousBits, bid) => (bid.mediaTypes && bid.mediaTypes[VIDEO]) ? (bid.mediaTypes[VIDEO].context == 'outstream' ? (previousBits | 2) : (previousBits | 1)) : previousBits, 0); // Only one type of auction is supported at a time @@ -286,7 +302,7 @@ function getSpaces(bidRequests, ml) { let sizeVast = firstSize ? firstSize.join('x') : DEFAULT_SIZE_VAST; name = 'video_' + sizeVast + '_' + i; es.map[name] = bid.bidId; - return name + ':' + sizeVast + ';1'; + return name + ':' + sizeVast + ';1' + getFloorStr(bid); } if (ml) { @@ -296,7 +312,7 @@ function getSpaces(bidRequests, ml) { } es.map[name] = bid.bidId; - return name + ':' + getSize(bid); + return name + ':' + getSize(bid) + getFloorStr(bid); }).join('+')).join('+'); return es; } diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 88d8f95b859..81b8c5d8058 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -1,7 +1,8 @@ -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'eskimi'; // const ENDPOINT = 'https://hb.eskimi.com/bids' @@ -65,7 +66,7 @@ const CONVERTER = ortbConverter({ imp.secure = Number(window.location.protocol === 'https:'); if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; - imp.bidfloorcur = utils.getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } if (bidRequest.mediaTypes[VIDEO]) { diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index 6a3a0869c0e..a29da69d6c7 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -12,7 +12,7 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // RE below lint exception: UID2 and EUID are separate modules, but the protocol is the same and shared code makes sense here. // eslint-disable-next-line prebid/validate-imports -import { Uid2GetId, Uid2CodeVersion } from './uid2IdSystem_shared.js'; +import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; @@ -99,6 +99,14 @@ export const euidIdSubmodule = { internalStorage: ADVERTISING_COOKIE }; + if (FEATURES.UID2_CSTG) { + mappedConfig.cstg = { + serverPublicKey: config?.params?.serverPublicKey, + subscriptionId: config?.params?.subscriptionId, + ...extractIdentityFromParams(config?.params ?? {}) + } + } + _logInfo(`EUID configuration loaded and mapped.`, mappedConfig); const result = Uid2GetId(mappedConfig, storage, _logInfo, _logWarn); _logInfo(`EUID getId returned`, result); return result; diff --git a/modules/euidIdSystem.md b/modules/euidIdSystem.md index e3e16bce89d..72e40b8ce7b 100644 --- a/modules/euidIdSystem.md +++ b/modules/euidIdSystem.md @@ -1,9 +1,59 @@ ## EUID User ID Submodule -EUID requires initial tokens to be generated server-side. The EUID module handles storing, providing, and optionally refreshing them. The module can operate in one of two different modes: *Client Refresh* mode or *Server Only* mode. +The EUID module handles storing, providing, and optionally refreshing tokens. While initial tokens traditionally required server-side generation, the introduction of the *Client-Side Token Generation (CSTG)* mode offers publishers the flexibility to generate EUID tokens directly from the module, eliminating this need. Publishers can choose to operate the module in one of three distinct modes: *Client Refresh* mode, *Server Only* mode and *Client-Side Token Generation* mode. *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. +*Client-Side Token Generation* mode is included in EUID module by default. However, it's important to note that this mode 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: + +``` + $ gulp build --modules=uid2IdSystem --disable UID2_CSTG +``` +If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the EUID Team first as they will provide required configuration values for you to use (see the Client-Side Token Generation (CSTG) mode section below for details) + +**This mode is created and made available recently. Please consult EUID Team first as they will provide required configuration values for you to use.** + +For publishers seeking a purely client-side integration without the complexities of server-side involvement, the CSTG mode is highly recommended. This mode requires the provision of a public key, subscription ID and [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) - either emails or phone numbers. In the CSTG mode, the module takes on the responsibility of encrypting the DII, generating the EUID token, and handling token refreshes when necessary. + +To configure the module to use this mode, you must: +1. Set `parmas.serverPublicKey` and `params.subscriptionId` (please reach out to the UID2 team to obtain these values) +2. Provide **ONLY ONE DII** by setting **ONLY ONE** of `params.email`/`params.phone`/`params.emailHash`/`params.phoneHash` + +Below is a table that provides guidance on when to use each directly identifying information (DII) parameter, along with information on whether normalization and hashing are required by the publisher for each parameter. + +| DII param | When to use it | Normalization required by publisher? | Hashing required by publisher? | +|------------------|-------------------------------------------------------|--------------------------------------|--------------------------------| +| params.email | When you have users' email address | No | No | +| params.phone | When you have user's phone number | Yes | No | +| params.emailHash | When you have user's hashed, normalized email address | Yes | Yes | +| params.phoneHash | When you have user's hashed, normalized phone number | Yes | Yes | + + +*Note that setting params.email will normalize email addresses, but params.phone requires phone numbers to be normalized.* + +Refer to [Normalization and Encoding](#normalization-and-encoding) for details on email address normalization, SHA-256 hashing and Base64 encoding. + +### CSTG example + +Configuration: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'euid', + params: { + serverPublicKey: '...server public key...', + subscriptionId: '...subcription id...', + email: 'user@email.com', + //phone: '+0000000', + //emailHash: '...email hash...', + //phoneHash: '...phone hash ...' + } + }] + } +}); +``` + ## Client Refresh mode This is the recommended mode for most scenarios. In this mode, the full response body from the EUID Token Generate or Token Refresh endpoint must be provided to the module. As long as the refresh token remains valid, the module will refresh the advertising token as needed. diff --git a/modules/experianRtdProvider.js b/modules/experianRtdProvider.js new file mode 100644 index 00000000000..e18296342de --- /dev/null +++ b/modules/experianRtdProvider.js @@ -0,0 +1,135 @@ +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { + deepAccess, + isArray, + isPlainObject, + isStr, + mergeDeep, + safeJSONParse, + timestamp +} from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; + +export const SUBMODULE_NAME = 'experian_rtid'; +export const EXPERIAN_RTID_DATA_KEY = 'experian_rtid_data'; +export const EXPERIAN_RTID_EXPIRATION_KEY = 'experian_rtid_expiration'; +export const EXPERIAN_RTID_STALE_KEY = 'experian_rtid_stale'; +export const EXPERIAN_RTID_NO_TRACK_KEY = 'experian_rtid_no_track'; +const EXPERIAN_RTID_URL = 'https://rtid.tapad.com' +const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); + +export const experianRtdObj = { + /** + * @summary modify bid request data + * @param {Object} reqBidsConfigObj + * @param {function} done + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + getBidRequestData(reqBidsConfigObj, done, config, userConsent) { + const dataEnvelope = storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + const stale = storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null); + const expired = storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null); + const noTrack = storage.getDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + const now = timestamp() + if (now > new Date(expired).getTime() || (noTrack == null && dataEnvelope == null)) { + // request data envelope and don't manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent) + done(); + return false; + } + if (now > new Date(stale).getTime()) { + // request data envelope and manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent); + } + if (noTrack != null) { + done(); + return false; + } + experianRtdObj.alterBids(reqBidsConfigObj, config); + done() + return true; + }, + + alterBids(reqBidsConfigObj, config) { + const dataEnvelope = safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null)); + if (dataEnvelope == null) { + return; + } + deepAccess(config, 'params.bidders').forEach((bidderCode) => { + const bidderData = dataEnvelope.find(({ bidder }) => bidder === bidderCode) + if (bidderData != null) { + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: { experianRtidKey: bidderData.data.key, experianRtidData: bidderData.data.data } }) + } + }) + }, + requestDataEnvelope(config, userConsent) { + function storeDataEnvelopeResponse(response) { + const responseJson = safeJSONParse(response); + if (responseJson != null) { + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, responseJson.staleAt, null); + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, responseJson.expiresAt, null); + if (responseJson.status === 'no_track') { + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + } else { + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify(responseJson.data), null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + } + } + } + const queryString = experianRtdObj.extractConsentQueryString(config, userConsent) + const fullUrl = queryString == null ? `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids` : `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids${queryString}` + ajax(fullUrl, storeDataEnvelopeResponse, null, { withCredentials: true, contentType: 'application/json' }) + }, + extractConsentQueryString(config, userConsent) { + const queryObj = {}; + + if (userConsent != null) { + if (userConsent.gdpr != null) { + const { gdprApplies, consentString } = userConsent.gdpr; + mergeDeep(queryObj, {gdpr: gdprApplies, gdpr_consent: consentString}) + } + if (userConsent.uspConsent != null) { + mergeDeep(queryObj, {us_privacy: userConsent.uspConsent}) + } + } + const consentQueryString = Object.entries(queryObj).map(([key, val]) => `${key}=${val}`).join('&'); + + let idsString = ''; + if (deepAccess(config, 'params.ids') != null && isPlainObject(deepAccess(config, 'params.ids'))) { + idsString = Object.entries(deepAccess(config, 'params.ids')).map(([idType, val]) => { + if (isArray(val)) { + return val.map((singleVal) => `id.${idType}=${singleVal}`).join('&') + } else { + return `id.${idType}=${val}` + } + }).join('&') + } + + const combinedString = [consentQueryString, idsString].filter((string) => string !== '').join('&'); + return combinedString !== '' ? `?${combinedString}` : undefined; + }, + /** + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @return {boolean} false to remove sub module + */ + init(config, userConsent) { + return isStr(deepAccess(config, 'params.accountId')); + } +} + +/** @type {RtdSubmodule} */ +export const experianRtdSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: experianRtdObj.getBidRequestData, + init: experianRtdObj.init +} + +submodule('realTimeData', experianRtdSubmodule); diff --git a/modules/experianRtdProvider.md b/modules/experianRtdProvider.md new file mode 100644 index 00000000000..ad46e0c3d55 --- /dev/null +++ b/modules/experianRtdProvider.md @@ -0,0 +1,52 @@ +# Experian Real-time Data Submodule + +## Overview + + Module Name: Experian Rtd Provider + Module Type: Rtd Provider + Maintainer: team-ui@tapad.com + +## Description + +The Experian RTD module adds encrypted identifier envelope to the bidding object. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,experianRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Experian RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Experian RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'experian_rtid', + waitForIt: true, + params: { + accountId: 'ZylatYg', + bidders: ['sovrn', 'pubmatic'], + ids: { maid: ['424', '2982'], hem: 'my-hem' } + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'experian_rtid' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.accountId | String | Your account id issued by Experian | | +| params.bidders | Array | List of bidders for which you would like data to be set | | +| params.ids | Record or string> | Additional identifiers to send to Experian RTID endpoint | | diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index e592cd38044..7162f4e9230 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -4,23 +4,31 @@ */ import { config } from '../src/config.js'; import { getHook } from '../src/hook.js'; -import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.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 {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'fledgeForGpt' +const PENDING = {}; export let isEnabled = false; config.getConfig('fledgeForGpt', config => init(config.fledgeForGpt)); /** - * Module init. - */ + * 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); @@ -28,28 +36,80 @@ export function init(cfg) { 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; } logInfo(`${MODULE} disabled`, cfg); } } -export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) { - const seller = componentAuctionConfig.seller; +function setComponentAuction(adUnitCode, auctionConfigs) { const gptSlot = getGptSlotForAdUnitCode(adUnitCode); if (gptSlot && gptSlot.setConfig) { gptSlot.setConfig({ - componentAuction: [{ - configKey: seller, - auctionConfig: componentAuctionConfig - }] + componentAuction: auctionConfigs.map(cfg => ({ + configKey: cfg.seller, + auctionConfig: cfg + })) }); - logInfo(MODULE, `register component auction config for: ${adUnitCode} x ${seller}: ${gptSlot.getAdUnitPath()}`, componentAuctionConfig); + logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); } else { - logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`); + logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); } +} - next(adUnitCode, componentAuctionConfig); +function onAuctionInit({auctionId}) { + PENDING[auctionId] = {}; +} + +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}) { + 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))) + }) + } 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() { @@ -60,25 +120,21 @@ export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { const globalFledgeConfig = config.getConfig('fledgeForGpt'); const bidders = globalFledgeConfig?.bidders ?? []; - bidderRequests.forEach((req) => { - const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length == 0 || bidders.includes(req.bidderCode)); - Object.assign(req, config.runWithBidder(req.bidderCode, () => { - return { - fledgeEnabled: config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined), - defaultForSlots: config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined) - } - })); + 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 (context.bidderRequest.fledgeEnabled) { - imp.ext = Object.assign(imp.ext || {}, { - ae: imp.ext?.ae ?? context.bidderRequest.defaultForSlots - }) - } else { + if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { delete imp.ext?.ae; } } diff --git a/modules/flippBidAdapter.js b/modules/flippBidAdapter.js new file mode 100644 index 00000000000..ea5e71ad81d --- /dev/null +++ b/modules/flippBidAdapter.js @@ -0,0 +1,186 @@ +import {isEmpty, parseUrl} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; + +const NETWORK_ID = 10922; +const AD_TYPES = [4309, 641]; +const DTX_TYPES = [5061]; +const TARGET_NAME = 'inline'; +const BIDDER_CODE = 'flipp'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +const DEFAULT_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_CREATIVE_TYPE = 'NativeX'; +const VALID_CREATIVE_TYPES = ['DTX', 'NativeX']; +const FLIPP_USER_KEY = 'flipp-uid'; +const COMPACT_DEFAULT_HEIGHT = 600; + +let userKey = null; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function getUserKey(options = {}) { + if (userKey) { + return userKey; + } + + // If the partner provides the user key use it, otherwise fallback to cookies + if ('userKey' in options && options.userKey) { + if (isValidUserKey(options.userKey)) { + userKey = options.userKey; + return options.userKey; + } + } + + // Grab from Cookie + const foundUserKey = storage.cookiesAreEnabled(null) && storage.getCookie(FLIPP_USER_KEY, null); + if (foundUserKey && isValidUserKey(foundUserKey)) { + return foundUserKey; + } + + // Generate if none found + userKey = generateUUID(); + + // Set cookie + if (storage.cookiesAreEnabled()) { + storage.setCookie(FLIPP_USER_KEY, userKey); + } + + return userKey; +} + +function isValidUserKey(userKey) { + return typeof userKey === 'string' && !userKey.startsWith('#') && userKey.length > 0; +} + +const generateUUID = () => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}; + +/** + * Determines if a creativeType is valid + * + * @param {string} creativeType The Creative Type to validate. + * @return string creativeType if this is a valid Creative Type, and 'NativeX' otherwise. + */ +const validateCreativeType = (creativeType) => { + if (creativeType && VALID_CREATIVE_TYPES.includes(creativeType)) { + return creativeType; + } else { + return DEFAULT_CREATIVE_TYPE; + } +}; + +const getAdTypes = (creativeType) => { + if (creativeType === 'DTX') { + return DTX_TYPES; + } + return AD_TYPES; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid.params.siteId) && !!(bid.params.publisherNameIdentifier); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests[] an array of bids + * @param {BidderRequest} bidderRequest master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const urlParams = parseUrl(bidderRequest.refererInfo.page).search; + const contentCode = urlParams['flipp-content-code']; + const userKey = getUserKey(validBidRequests[0]?.params); + const placements = validBidRequests.map((bid, index) => { + const options = bid.params.options || {}; + if (!options.hasOwnProperty('startCompact')) { + options.startCompact = true; + } + return { + divName: TARGET_NAME, + networkId: NETWORK_ID, + siteId: bid.params.siteId, + adTypes: getAdTypes(bid.params.creativeType), + count: 1, + ...(!isEmpty(bid.params.zoneIds) && {zoneIds: bid.params.zoneIds}), + properties: { + ...(!isEmpty(contentCode) && {contentCode: contentCode.slice(0, 32)}), + }, + options, + prebid: { + requestId: bid.bidId, + publisherNameIdentifier: bid.params.publisherNameIdentifier, + height: bid.mediaTypes.banner.sizes[index][0], + width: bid.mediaTypes.banner.sizes[index][1], + creativeType: validateCreativeType(bid.params.creativeType), + } + } + }); + return { + method: 'POST', + url: ENDPOINT, + data: { + placements, + url: bidderRequest.refererInfo.page, + user: { + key: userKey, + }, + }, + } + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest A bid request object + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse?.body) return []; + const placements = bidRequest.data.placements; + const res = serverResponse.body; + if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) { + return res.decisions.inline.map(decision => { + const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId); + const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height; + return { + bidderCode: BIDDER_CODE, + requestId: decision.prebid?.requestId, + cpm: decision.prebid?.cpm, + width: decision.width, + height, + creativeId: decision.adId, + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: DEFAULT_TTL, + ad: decision.prebid?.creative, + } + }); + } + return []; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: (syncOptions, serverResponses) => [], +} +registerBidder(spec); diff --git a/modules/flippBidAdapter.md b/modules/flippBidAdapter.md new file mode 100644 index 00000000000..e823432a60f --- /dev/null +++ b/modules/flippBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Flipp Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@flipp.com +``` + +# Description + +This module connects publishers to Flipp's Shopper Experience via Prebid.js. + + +# Test parameters + +```javascript +var adUnits = [ + { + code: 'flipp-scroll-ad-content', + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + bids: [ + { + bidder: 'flipp', + params: { + creativeType: 'NativeX', // Optional, can be one of 'NativeX' (default) or 'DTX' + publisherNameIdentifier: 'wishabi-test-publisher', // Required + siteId: 1192075, // Required + zoneIds: [260678], // Optional + userKey: ``, // Optional, but recommended for better user experience. Can be a cookie, session id or any other user identifier + options: { + startCompact: true, // Optional. Height of the experience will be reduced. Default to true + dwellExpand: true // Optional. Auto expand the experience after a certain time passes. Default to true + } + } + } + ] + } +] +``` diff --git a/modules/freepassIdSystem.js b/modules/freepassIdSystem.js index d52c537e800..419aa9ec414 100644 --- a/modules/freepassIdSystem.js +++ b/modules/freepassIdSystem.js @@ -1,8 +1,12 @@ import { submodule } from '../src/hook.js'; -import {generateUUID, logMessage} from '../src/utils.js'; +import { logMessage } from '../src/utils.js'; +import { getCoreStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'freepassId'; +export const FREEPASS_COOKIE_KEY = '_f_UF8cCRlr'; +export const storage = getCoreStorageManager(MODULE_NAME); + export const freepassIdSubmodule = { name: MODULE_NAME, decode: function (value, config) { @@ -15,7 +19,12 @@ export const freepassIdSubmodule = { logMessage('Getting FreePass ID using config: ' + JSON.stringify(config)); const freepassData = config.params !== undefined ? (config.params.freepassData || {}) : {} - let idObject = {userId: generateUUID()}; + const idObject = {}; + + const userId = storage.getCookie(FREEPASS_COOKIE_KEY); + if (userId !== null) { + idObject.userId = userId; + } if (freepassData.commonId !== undefined) { idObject.commonId = config.params.freepassData.commonId; @@ -29,8 +38,8 @@ export const freepassIdSubmodule = { }, extendId: function (config, consent, cachedIdObject) { - let freepassData = config.params.freepassData; - let hasFreepassData = freepassData !== undefined; + const freepassData = config.params.freepassData; + const hasFreepassData = freepassData !== undefined; if (!hasFreepassData) { logMessage('No Freepass Data. CachedIdObject will not be extended: ' + JSON.stringify(cachedIdObject)); return { @@ -38,12 +47,7 @@ export const freepassIdSubmodule = { }; } - if (freepassData.commonId === cachedIdObject.commonId && freepassData.userIp === cachedIdObject.userIp) { - logMessage('FreePass ID is already up-to-date: ' + JSON.stringify(cachedIdObject)); - return { - id: cachedIdObject - }; - } + const currentCookieId = storage.getCookie(FREEPASS_COOKIE_KEY); logMessage('Extending FreePass ID object: ' + JSON.stringify(cachedIdObject)); logMessage('Extending FreePass ID using config: ' + JSON.stringify(config)); @@ -52,8 +56,8 @@ export const freepassIdSubmodule = { id: { commonId: freepassData.commonId, userIp: freepassData.userIp, - userId: cachedIdObject.userId, - }, + userId: currentCookieId + } }; } }; diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index cd4785cdc78..73c56160b00 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -4,6 +4,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; const BIDDER_CODE = 'freewheel-ssp'; +const GVL_ID = 285; const PROTOCOL = getProtocol(); const FREEWHEEL_ADSSETUP = PROTOCOL + '://ads.stickyadstv.com/www/delivery/swfIndex.php'; @@ -182,8 +183,8 @@ function getCampaignId(xmlNode) { } /** -* returns the top most accessible window -*/ + * returns the top most accessible window + */ function getTopMostWindow() { var res = window; @@ -314,24 +315,25 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: ['stickyadstv', 'freewheelssp'], // aliases for freewheel-ssp /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { return !!(bid.params.zoneId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(bidRequests, bidderRequest) { // var currency = config.getConfig(currency); @@ -382,6 +384,15 @@ export const spec = { requestParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; } + // Add content object + if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.site && bidderRequest.ortb2.site.content && typeof bidderRequest.ortb2.site.content === 'object') { + try { + requestParams._fw_prebid_content = JSON.stringify(bidderRequest.ortb2.site.content); + } catch (error) { + logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the content object: ' + error); + } + } + // Add schain object var schain = currentBidRequest.schain; if (schain) { @@ -463,12 +474,12 @@ export const spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @param {object} request: the built request object containing the initial bidRequest. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {object} request: the built request object containing the initial bidRequest. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, request) { var bidrequest = request.bidRequest; var playerSize = []; @@ -532,10 +543,11 @@ export const spec = { }; if (bidrequest.mediaTypes.video) { - bidResponse.vastXml = serverResponse; bidResponse.mediaType = 'video'; } + bidResponse.vastXml = serverResponse; + bidResponse.ad = formatAdHTML(bidrequest, playerSize); bidResponses.push(bidResponse); } diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index e7f1e7b8b45..5b73ec19e08 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -5,10 +5,9 @@ import {deepAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; -import {find} from '../src/polyfill.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID, FIRST_PARTY_GVLID} from '../src/consentHandler.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, @@ -27,44 +26,62 @@ import { ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS, - ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_UFPD + ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD } from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; -const TCF2 = { - purpose1: {id: 1, name: 'storage'}, - purpose2: {id: 2, name: 'basicAds'}, - purpose4: {id: 4, name: 'personalizedAds'}, - purpose7: {id: 7, name: 'measurement'}, +export const ACTIVE_RULES = { + purpose: {}, + feature: {} }; -/* - These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher. -*/ -const DEFAULT_RULES = [{ - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}, { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}]; - -export let purpose1Rule; -export let purpose2Rule; -export let purpose4Rule; -export let purpose7Rule; - -export let enforcementRules; +const CONSENT_PATHS = { + purpose: 'purpose.consents', + feature: 'specialFeatureOptins' +}; + +const CONFIGURABLE_RULES = { + storage: { + type: 'purpose', + default: { + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }, + id: 1, + }, + basicAds: { + type: 'purpose', + id: 2, + default: { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + } + }, + personalizedAds: { + type: 'purpose', + id: 4, + }, + measurement: { + type: 'purpose', + id: 7, + }, + transmitPreciseGeo: { + type: 'feature', + id: 1, + }, +}; const storageBlocked = new Set(); const biddersBlocked = new Set(); const analyticsBlocked = new Set(); const ufpdBlocked = new Set(); +const eidsBlocked = new Set(); +const geoBlocked = new Set(); let hooksAdded = false; let strictStorageEnforcement = false; @@ -79,6 +96,9 @@ const GVLID_LOOKUP_PRIORITY = [ const RULE_NAME = 'TCF2'; const RULE_HANDLES = []; +// in JS we do not have access to the GVL; assume that everyone declares legitimate interest for basic ads +const LI_PURPOSES = [2]; + /** * Retrieve a module's GVL ID. */ @@ -91,7 +111,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { if (gvlMapping && gvlMapping[moduleName]) { return gvlMapping[moduleName]; } else if (moduleType === MODULE_TYPE_PREBID) { - return VENDORLESS_GVLID; + return moduleName === 'cdep' ? FIRST_PARTY_GVLID : VENDORLESS_GVLID; } else { let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); if (gvlid == null && Object.keys(modules).length > 0) { @@ -143,6 +163,17 @@ export function shouldEnforce(consentData, purpose, name) { return consentData && consentData.gdprApplies; } +function getConsent(consentData, type, id, gvlId) { + let purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${id}`); + let vendor = !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); + + if (type === 'purpose' && LI_PURPOSES.includes(id)) { + purpose ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${id}`); + vendor ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`); + } + return {purpose, vendor}; +} + /** * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, * the caller may decide to suppress a TCF-sensitive activity. @@ -153,42 +184,32 @@ export function shouldEnforce(consentData, purpose, name) { * @returns {boolean} */ export function validateRules(rule, consentData, currentModule, gvlId) { - const purposeId = TCF2[Object.keys(TCF2).filter(purposeName => TCF2[purposeName].name === rule.purpose)[0]].id; + const ruleOptions = CONFIGURABLE_RULES[rule.purpose]; // return 'true' if vendor present in 'vendorExceptions' if ((rule.vendorExceptions || []).includes(currentModule)) { return true; } const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); + const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId); - let purposeAllowed = !rule.enforcePurpose || !!deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); - let vendorAllowed = !vendorConsentRequred || !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); + let validation = (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); - if (purposeId === 2) { - purposeAllowed ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`); - vendorAllowed ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`); + if (gvlId === FIRST_PARTY_GVLID) { + validation = (!rule.enforcePurpose || !!deepAccess(consentData, `vendorData.publisher.consents.${ruleOptions.id}`)); } - return purposeAllowed && vendorAllowed; + return validation; } -/** - * all activity rules follow the same structure: - * if GDPR is in scope, check configuration for a particular purpose, and if that enables enforcement, - * check against consent data for that purpose and vendor - * - * @param purposeNo TCF purpose number to check for this activity - * @param getEnforcementRule getter for gdprEnforcement rule definition to use - * @param blocked optional set to use for collecting denied vendors - * @param gvlidFallback optional factory function for a gvlid falllback function - */ -function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = () => null) { +function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback = () => null) { return function (params) { const consentData = gdprDataHandler.getConsentData(); const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; + if (shouldEnforce(consentData, purposeNo, modName)) { const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params)); - let allow = !!validateRules(getEnforcementRule(), consentData, modName, gvlid); + let allow = !!checkConsent(consentData, modName, gvlid); if (!allow) { blocked && blocked.add(modName); return {allow}; @@ -197,32 +218,62 @@ function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = }; } -export const accessDeviceRule = ((rule) => { - return function (params) { - // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; - return rule(params); - }; -})(gdprRule(1, () => purpose1Rule, storageBlocked)); - -export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); -export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked); +function singlePurposeGdprRule(purposeNo, blocked = null, gvlidFallback = () => null) { + return gdprRule(purposeNo, (cd, modName, gvlid) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid), blocked, gvlidFallback); +} -export const fetchBidsRule = ((rule) => { +function exceptPrebidModules(ruleFn) { return function (params) { - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID) { // TODO: this special case is for the PBS adapter (componentType is 'prebid') // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; // that is, however, a breaking change and skipped for now return; } + return ruleFn(params); + }; +} + +export const accessDeviceRule = ((rule) => { + return function (params) { + // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; return rule(params); }; -})(gdprRule(2, () => purpose2Rule, biddersBlocked)); +})(singlePurposeGdprRule(1, storageBlocked)); + +export const syncUserRule = singlePurposeGdprRule(1, storageBlocked); +export const enrichEidsRule = singlePurposeGdprRule(1, storageBlocked); +export const fetchBidsRule = exceptPrebidModules(singlePurposeGdprRule(2, biddersBlocked)); +export const reportAnalyticsRule = singlePurposeGdprRule(7, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); +export const ufpdRule = singlePurposeGdprRule(4, ufpdBlocked); + +export const transmitEidsRule = exceptPrebidModules((() => { + // Transmit EID special case: + // by default, legal basis or vendor exceptions for any purpose between 2 and 10 + // (but disregarding enforcePurpose and enforceVendor config) is enough to allow EIDs through + function check2to10Consent(consentData, modName, gvlId) { + for (let pno = 2; pno <= 10; pno++) { + if (ACTIVE_RULES.purpose[pno]?.vendorExceptions?.includes(modName)) { + return true; + } + const {purpose, vendor} = getConsent(consentData, 'purpose', pno, gvlId); + if (purpose && (vendor || ACTIVE_RULES.purpose[pno]?.softVendorExceptions?.includes(modName))) { + return true; + } + } + return false; + } -export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); + const defaultBehavior = gdprRule('2-10', check2to10Consent, eidsBlocked); + const p4Behavior = singlePurposeGdprRule(4, eidsBlocked); + return function () { + const fn = ACTIVE_RULES.purpose[4]?.eidsRequireP4Consent ? p4Behavior : defaultBehavior; + return fn.apply(this, arguments); + }; +})()); -export const ufpdRule = gdprRule(4, () => purpose4Rule, ufpdBlocked); +export const transmitPreciseGeoRule = gdprRule('Special Feature 1', (cd, modName, gvlId) => validateRules(ACTIVE_RULES.feature[1], cd, modName, gvlId), geoBlocked); /** * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. @@ -237,65 +288,55 @@ function emitTCF2FinalResults() { biddersBlocked: formatSet(biddersBlocked), analyticsBlocked: formatSet(analyticsBlocked), ufpdBlocked: formatSet(ufpdBlocked), + eidsBlocked: formatSet(eidsBlocked), + geoBlocked: formatSet(geoBlocked) }; events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); - [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked].forEach(el => el.clear()); + [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked, eidsBlocked, geoBlocked].forEach(el => el.clear()); } events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); -function hasPurpose(purposeNo) { - const pname = TCF2[`purpose${purposeNo}`].name; - return (rule) => rule.purpose === pname; -} - /** * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object */ export function setEnforcementConfig(config) { - const rules = deepAccess(config, 'gdpr.rules'); + let rules = deepAccess(config, 'gdpr.rules'); if (!rules) { logWarn('TCF2: enforcing P1 and P2 by default'); - enforcementRules = DEFAULT_RULES; - } else { - enforcementRules = rules; } + rules = Object.fromEntries((rules || []).map(r => [r.purpose, r])); strictStorageEnforcement = !!deepAccess(config, STRICT_STORAGE_ENFORCEMENT); - purpose1Rule = find(enforcementRules, hasPurpose(1)); - purpose2Rule = find(enforcementRules, hasPurpose(2)); - purpose4Rule = find(enforcementRules, hasPurpose(4)) - purpose7Rule = find(enforcementRules, hasPurpose(7)); - - if (!purpose1Rule) { - purpose1Rule = DEFAULT_RULES[0]; - } - - if (!purpose2Rule) { - purpose2Rule = DEFAULT_RULES[1]; - } + Object.entries(CONFIGURABLE_RULES).forEach(([name, opts]) => { + ACTIVE_RULES[opts.type][opts.id] = rules[name] ?? opts.default; + }); if (!hooksAdded) { - if (purpose1Rule) { + if (ACTIVE_RULES.purpose[1] != null) { hooksAdded = true; RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); } - if (purpose2Rule) { + if (ACTIVE_RULES.purpose[2] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } - if (purpose4Rule) { + if (ACTIVE_RULES.purpose[4] != null) { RULE_HANDLES.push( registerActivityControl(ACTIVITY_TRANSMIT_UFPD, RULE_NAME, ufpdRule), registerActivityControl(ACTIVITY_ENRICH_UFPD, RULE_NAME, ufpdRule) ); } - if (purpose7Rule) { + if (ACTIVE_RULES.purpose[7] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)); } + if (ACTIVE_RULES.feature[1] != null) { + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_PRECISE_GEO, RULE_NAME, transmitPreciseGeoRule)); + } + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_EIDS, RULE_NAME, transmitEidsRule)); } } diff --git a/modules/genericAnalyticsAdapter.js b/modules/genericAnalyticsAdapter.js index b52cb7e5464..7f721863912 100644 --- a/modules/genericAnalyticsAdapter.js +++ b/modules/genericAnalyticsAdapter.js @@ -1,6 +1,6 @@ import AnalyticsAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import {prefixLog, isPlainObject} from '../src/utils.js'; -import * as CONSTANTS from '../src/constants.json'; +import {has as hasEvent} from '../src/events.js'; import adapterManager from '../src/adapterManager.js'; import {ajaxBuilder} from '../src/ajax.js'; @@ -48,12 +48,12 @@ export function GenericAnalytics() { return false; } for (const [event, handler] of Object.entries(options.events)) { - if (!CONSTANTS.EVENTS.hasOwnProperty(event)) { + if (!hasEvent(event)) { logWarn(`options.events.${event} does not match any known Prebid event`); - if (typeof handler !== 'function') { - logError(`options.events.${event} must be a function`); - return false; - } + } + if (typeof handler !== 'function') { + logError(`options.events.${event} must be a function`); + return false; } } } diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 646d2f4e786..37db5860001 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -17,11 +17,12 @@ import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; +import { generateUUID, createInvisibleIframe, insertElement, isEmpty, logError } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import { loadExternalScript } from '../src/adloader.js'; import { auctionManager } from '../src/auctionManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; /** @type {string} */ const SUBMODULE_NAME = 'geoedge'; @@ -69,17 +70,38 @@ export function setWrapper(responseText) { wrapper = responseText; } +export function getInitialParams(key) { + let refererInfo = getRefererInfo(); + let params = { + wver: 'pbjs', + wtype: 'pbjs-module', + key, + meta: { + topUrl: refererInfo.page + }, + site: refererInfo.domain, + pimp: PV_ID, + fsRan: true, + frameApi: true + }; + return params; +} + +export function markAsLoaded() { + preloaded = true; +} + /** * preloads the client - * @param {string} key + * @param {string} key */ export function preloadClient(key) { - let link = document.createElement('link'); - link.rel = 'preload'; - link.as = 'script'; - link.href = getClientUrl(key); - link.onload = () => { preloaded = true }; - insertElement(link); + let iframe = createInvisibleIframe(); + iframe.id = 'grumiFrame'; + insertElement(iframe); + iframe.contentWindow.grumi = getInitialParams(key); + let url = getClientUrl(key); + loadExternalScript(url, SUBMODULE_NAME, markAsLoaded, iframe.contentDocument); } /** @@ -103,7 +125,7 @@ export function wrapHtml(wrapper, html) { * @param {string} key * @return {Object} */ -function getMacros(bid, key) { +export function getMacros(bid, key) { return { '${key}': key, '%%ADUNIT%%': bid.adUnitCode, @@ -116,7 +138,9 @@ function getMacros(bid, key) { '%_hbadomains': bid.meta && bid.meta.advertiserDomains, '%%PATTERN:hb_pb%%': bid.pbHg, '%%SITE%%': location.hostname, - '%_pimp%': PV_ID + '%_pimp%': PV_ID, + '%_hbCpm!': bid.cpm, + '%_hbCurrency!': bid.currency }; } @@ -250,9 +274,9 @@ function init(config, userConsent) { /** @type {RtdSubmodule} */ export const geoedgeSubmodule = { /** - * used to link submodule with realTimeData - * @type {string} - */ + * used to link submodule with realTimeData + * @type {string} + */ name: SUBMODULE_NAME, init, onBidResponseEvent: conditionallyWrap diff --git a/modules/geolocationRtdProvider.js b/modules/geolocationRtdProvider.js index b46a25e9246..6bfed7ee934 100644 --- a/modules/geolocationRtdProvider.js +++ b/modules/geolocationRtdProvider.js @@ -4,6 +4,7 @@ import { ACTIVITY_TRANSMIT_PRECISE_GEO } from '../src/activities/activities.js'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { isActivityAllowed } from '../src/activities/rules.js'; import { activityParams } from '../src/activities/activityParams.js'; +import {VENDORLESS_GVLID} from '../src/consentHandler.js'; let permissionsAvailable = true; let geolocation; @@ -54,6 +55,7 @@ function init(moduleConfig) { } export const geolocationSubmodule = { name: 'geolocation', + gvlid: VENDORLESS_GVLID, getBidRequestData: getGeolocationData, init: init, }; diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 25322d81f9b..8cffccb9f30 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,4 +1,4 @@ -import { getBidIdParameter, isFn, isInteger } from '../src/utils.js'; +import {getBidIdParameter, isFn, isInteger} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'getintent'; @@ -38,7 +38,7 @@ export const spec = { * * @param {BidRequest} bid The bid to validate. * @return {boolean} True if this is a valid bid, and false otherwise. - * */ + */ isBidRequestValid: function(bid) { return !!(bid && bid.params && bid.params.pid && bid.params.tid); }, @@ -108,7 +108,7 @@ function buildUrl(bid) { * * @param {BidRequest} bidRequest. * @return {object} GI bid request. - * */ + */ function buildGiBidRequest(bidRequest) { let giBidRequest = { bid_id: bidRequest.bidId, @@ -191,7 +191,7 @@ function addOptional(params, request, props) { /** * @param {String} s The string representing a size (e.g. "300x250"). * @return {Number[]} An array with two elements: [width, height] (e.g.: [300, 250]). - * */ + */ function parseSize(s) { return s.split('x').map(Number); } @@ -200,7 +200,7 @@ function parseSize(s) { * @param {Array} sizes An array of sizes/numbers to be joined into single string. * May be an array (e.g. [300, 250]) or array of arrays (e.g. [[300, 250], [640, 480]]. * @return {String} The string with sizes, e.g. array of sizes [[50, 50], [80, 80]] becomes "50x50,80x80" string. - * */ + */ function produceSize (sizes) { function sizeToStr(s) { if (Array.isArray(s) && s.length === 2 && isInteger(s[0]) && isInteger(s[1])) { diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index 91ed5c9b3fb..86cbb1d89de 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.js @@ -116,11 +116,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index 8c90d0cccfe..559f9f77aaf 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,17 +1,16 @@ import { createTrackPixelHtml, deepAccess, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, getDNT, getWindowTop, isEmpty, - logError, - tryAppendQueryString + logError } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'gmossp'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 4768931950c..8892df130df 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -1,15 +1,9 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -29,9 +23,12 @@ import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { APPNEXUS_CATEGORY_MAPPING } from '../libraries/categoryTranslationMapping/index.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'goldbach'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -971,7 +968,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -997,7 +994,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/goldfishAdsRtdProvider.js b/modules/goldfishAdsRtdProvider.js new file mode 100755 index 00000000000..c90d3104b56 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.js @@ -0,0 +1,194 @@ +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess } from '../src/utils.js'; + +export const MODULE_NAME = 'goldfishAdsRtd'; +export const MODULE_TYPE = 'realTimeData'; +export const ENDPOINT_URL = 'https://prebid.goldfishads.com/iab-segments'; +export const DATA_STORAGE_KEY = 'goldfishads_data'; +export const DATA_STORAGE_TTL = 1800 * 1000// TTL in seconds + +export const ADAPTER_VERSION = '1.0'; + +export const storage = getStorageManager({ + gvlid: null, + moduleName: MODULE_NAME, + moduleType: MODULE_TYPE, +}); + +/** + * + * @param {{response: string[]} } response + * @returns + */ +export const manageCallbackResponse = (response) => { + try { + const foo = JSON.parse(response.response); + if (!Array.isArray(foo)) throw new Error('Invalid response'); + const enrichedResponse = { + ext: { + segtax: 4 + }, + segment: foo.map((segment) => { return { id: segment } }), + }; + const output = { + name: 'goldfishads.com', + ...enrichedResponse, + }; + return output; + } catch (e) { + throw e; + }; +}; + +/** + * @param {string} key + * @returns { Promise<{name: 'goldfishads.com', ext: { segtag: 4 }, segment: string[]}> } + */ + +const getTargetingDataFromApi = (key) => { + return new Promise((resolve, reject) => { + const requestOptions = { + customHeaders: { + 'Accept': 'application/json' + } + } + const callbacks = { + success(responseText, response) { + try { + const output = manageCallbackResponse(response); + resolve(output); + } catch (e) { + reject(e); + } + }, + error(error) { + reject(error); + } + }; + ajax(`${ENDPOINT_URL}?key=${key}`, callbacks, null, requestOptions) + }) +}; + +/** + * @returns {{ + * name: 'golfishads.com', + * ext: { segtax: 4}, + * segment: string[] + * } | null } + */ +export const getStorageData = () => { + const now = new Date(); + const data = storage.getDataFromLocalStorage(DATA_STORAGE_KEY); + if (data === null) return null; + try { + const foo = JSON.parse(data); + if (now.getTime() > foo.expiry) return null; + return foo.targeting; + } catch (e) { + return null; + } +}; + +/** + * @param { { key: string } } payload + * @returns {Promise<{ + * name: string, + * ext: { segtax: 4}, + * segment: string[] + * }> | null + * } + */ + +const getTargetingData = (payload) => new Promise((resolve) => { + const targeting = getStorageData(); + if (targeting === null) { + getTargetingDataFromApi(payload.key) + .then((response) => { + const now = new Date() + const data = { + targeting: response, + expiry: now.getTime() + DATA_STORAGE_TTL, + }; + storage.setDataInLocalStorage(DATA_STORAGE_KEY, JSON.stringify(data)); + resolve(response); + }) + .catch((e) => { + resolve(null); + }); + } else { + resolve(targeting); + } +}) + +/** + * + * @param {*} config + * @param {*} userConsent + * @returns {boolean} + */ + +const init = (config, userConsent) => { + if (!config.params || !config.params.key) return false; + // return { type: (typeof config.params.key === 'string') }; + if (!(typeof config.params.key === 'string')) return false; + return true; +}; + +/** + * + * @param {{ + * name: string, + * ext: { segtax: 4}, + * segment: {id: string}[] + * } | null } userData + * @param {*} reqBidsConfigObj + * @returns + */ +export const updateUserData = (userData, reqBidsConfigObj) => { + if (userData === null) return; + const bidders = ['appnexus', 'rubicon', 'nexx360']; + for (let i = 0; i < bidders.length; i++) { + const bidderCode = bidders[i]; + const originalConfig = deepAccess(reqBidsConfigObj, `ortb2Fragments.bidder[${bidderCode}].user.data`) || []; + const userConfig = [ + ...originalConfig, + userData, + ]; + reqBidsConfigObj.ortb2Fragments = reqBidsConfigObj.ortb2Fragments || {}; + reqBidsConfigObj.ortb2Fragments.bidder = reqBidsConfigObj.ortb2Fragments.bidder || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data || userConfig; + } + return reqBidsConfigObj; +} + +/** + * + * @param {*} reqBidsConfigObj + * @param {*} callback + * @param {*} moduleConfig + * @param {*} userConsent + * @returns {void} + */ +const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + const payload = { + key: moduleConfig.params.key, + }; + getTargetingData(payload) + .then((userData) => { + updateUserData(userData, reqBidsConfigObj); + callback(); + }); +}; + +/** @type {RtdSubmodule} */ +export const goldfishAdsSubModule = { + name: MODULE_NAME, + init, + getBidRequestData, +}; + +submodule(MODULE_TYPE, goldfishAdsSubModule); diff --git a/modules/goldfishAdsRtdProvider.md b/modules/goldfishAdsRtdProvider.md new file mode 100755 index 00000000000..4625c9a7988 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.md @@ -0,0 +1,48 @@ +# Goldfish Ads Real-time Data Submodule + +## Overview + + Module Name: Goldfish Ads Rtd Provider + Module Type: Rtd Provider + Maintainer: keith@goldfishads.com + +## Description + +This RTD module provides access to the Goldfish Ads Geograph, which leverages geographic and temporal data on a privcay-first platform. This module works without using cookies, PII, emails, or device IDs across all website traffic, including unauthenticated users, and adds audience data into bid requests to increase scale and yields. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,goldfishAdsRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Goldfish Ads RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Goldfish Ads RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'goldfishAds', + waitForIt: true, + params: { + key: 'testkey' + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'goldfishAds' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.key | String | Your key id issued by Goldfish Ads | | diff --git a/modules/gptPreAuction.js b/modules/gptPreAuction.js index 71884235b38..bf5b4a55dbb 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -1,4 +1,11 @@ -import {deepAccess, isAdUnitCodeMatchingSlot, isGptPubadsDefined, logInfo, pick} from '../src/utils.js'; +import { + deepAccess, + isAdUnitCodeMatchingSlot, + isGptPubadsDefined, + logInfo, + pick, + deepSetValue +} from '../src/utils.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; import {find} from '../src/polyfill.js'; @@ -15,7 +22,8 @@ export const appendGptSlots = adUnits => { } const adUnitMap = adUnits.reduce((acc, adUnit) => { - acc[adUnit.code] = adUnit; + acc[adUnit.code] = acc[adUnit.code] || []; + acc[adUnit.code].push(adUnit); return acc; }, {}); @@ -25,15 +33,13 @@ export const appendGptSlots = adUnits => { : isAdUnitCodeMatchingSlot(slot)); if (matchingAdUnitCode) { - const adUnit = adUnitMap[matchingAdUnitCode]; - adUnit.ortb2Imp = adUnit.ortb2Imp || {}; - adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; - adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; - - const context = adUnit.ortb2Imp.ext.data; - context.adserver = context.adserver || {}; - context.adserver.name = 'gam'; - context.adserver.adslot = sanitizeSlotPath(slot.getAdUnitPath()); + const adserver = { + name: 'gam', + adslot: sanitizeSlotPath(slot.getAdUnitPath()) + }; + adUnitMap[matchingAdUnitCode].forEach((adUnit) => { + deepSetValue(adUnit, 'ortb2Imp.ext.data.adserver', Object.assign({}, adUnit.ortb2Imp?.ext?.data?.adserver, adserver)); + }); } }); }; diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index 70031ebd06e..cc02c6a103e 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -16,16 +16,16 @@ export const cookieKey = 'gravitompId'; export const gravitoIdSystemSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * performs action to obtain id - * @function - * @returns { {id: {gravitompId: string}} | undefined } - */ + * performs action to obtain id + * @function + * @returns { {id: {gravitompId: string}} | undefined } + */ getId: function() { const newId = storage.getCookie(cookieKey); if (!newId) { @@ -38,11 +38,11 @@ export const gravitoIdSystemSubmodule = { }, /** - * decode the stored id value for passing to bid requests - * @function - * @param { {gravitompId: string} } value - * @returns { {gravitompId: {string} } | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { {gravitompId: string} } value + * @returns { {gravitompId: {string} } | undefined } + */ decode: function(value) { if (value && typeof value === 'object') { var result = {}; diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index edc0c9c6c5c..5d1f35f24ff 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -2,18 +2,20 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {deepClone, logError, logInfo} from '../src/utils.js'; +import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils.js'; const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '1.0.0'; +export const ANALYTICS_VERSION = '2.0.0'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; const { EVENTS: { + AUCTION_INIT, AUCTION_END, BID_TIMEOUT, + BILLABLE_EVENT, } } = CONSTANTS; @@ -25,28 +27,53 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const parseBidderCode = function (bid) { - let bidderCode = bid.bidderCode || bid.bidder; - return bidderCode.toLowerCase(); -}; +export const isSampled = function(greenbidsId, samplingRate) { + if (samplingRate < 0 || samplingRate > 1) { + logWarn('Sampling rate must be between 0 and 1'); + return true; + } + const hashInt = parseInt(greenbidsId.slice(-4), 16); + + return hashInt < samplingRate * (0xFFFF + 1); +} export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { cachedAuctions: {}, initConfig(config) { + analyticsOptions.options = deepClone(config.options); /** * Required option: pbuid * @type {boolean} */ - analyticsOptions.options = deepClone(config.options); - if (typeof config.options.pbuid !== 'string' || config.options.pbuid.length < 1) { + if (typeof analyticsOptions.options.pbuid !== 'string' || analyticsOptions.options.pbuid.length < 1) { logError('"options.pbuid" is required.'); return false; } + /** + * Deprecate use of integerated 'sampling' config + * replace by greenbidsSampling + */ + if (typeof analyticsOptions.options.sampling === 'number') { + logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.'); + analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling; + // Set sampling to null to prevent prebid analytics integrated sampling to happen + analyticsOptions.options.sampling = null; + } + + /** + * Discourage unsampled analytics + */ + if (typeof analyticsOptions.options.greenbidsSampling !== 'number' || analyticsOptions.options.greenbidsSampling >= 1) { + logWarn('"options.greenbidsSampling" is not set or >=1, using this analytics module unsampled is discouraged.'); + analyticsOptions.options.greenbidsSampling = 1; + } + analyticsOptions.pbuid = config.options.pbuid analyticsOptions.server = ANALYTICS_SERVER; + return true; }, sendEventMessage(endPoint, data) { @@ -57,13 +84,16 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }); }, createCommonMessage(auctionId) { + const cachedAuction = this.getCachedAuction(auctionId); return { version: ANALYTICS_VERSION, auctionId: auctionId, referrer: window.location.href, - sampling: analyticsOptions.options.sampling, + sampling: analyticsOptions.options.greenbidsSampling, prebid: '$prebid.version$', + greenbidsId: cachedAuction.greenbidsId, pbuid: analyticsOptions.pbuid, + billingId: cachedAuction.billingId, adUnits: [], }; }, @@ -96,22 +126,24 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER } } }, - createBidMessage(auctionEndArgs, timeoutBids) { - logInfo(auctionEndArgs) + createBidMessage(auctionEndArgs) { const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; + const cachedAuction = this.getCachedAuction(auctionId); const message = this.createCommonMessage(auctionId); + const timeoutBids = cachedAuction.timeoutBids || []; message.auctionElapsed = (auctionEnd - timestamp); adUnits.forEach((adUnit) => { - const adUnitCode = adUnit.code.toLowerCase(); + const adUnitCode = adUnit.code?.toLowerCase() || 'unknown_adunit_code'; message.adUnits.push({ code: adUnitCode, mediaTypes: { - ...(adUnit.mediaTypes.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, - ...(adUnit.mediaTypes.video !== undefined) && {video: adUnit.mediaTypes.video}, - ...(adUnit.mediaTypes.native !== undefined) && {native: adUnit.mediaTypes.native} + ...(adUnit.mediaTypes?.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, + ...(adUnit.mediaTypes?.video !== undefined) && {video: adUnit.mediaTypes.video}, + ...(adUnit.mediaTypes?.native !== undefined) && {native: adUnit.mediaTypes.native} }, + ortb2Imp: adUnit.ortb2Imp || {}, bidders: [], }); }); @@ -129,13 +161,26 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER getCachedAuction(auctionId) { this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || { timeoutBids: [], + greenbidsId: null, + billingId: null, + isSampled: true, }; return this.cachedAuctions[auctionId]; }, + handleAuctionInit(auctionInitArgs) { + const cachedAuction = this.getCachedAuction(auctionInitArgs.auctionId); + try { + cachedAuction.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId; + } catch (e) { + logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); + cachedAuction.greenbidsId = generateUUID(); + } + cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling); + }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); this.sendEventMessage('/', - this.createBidMessage(auctionEndArgs, cachedAuction.timeoutBids) + this.createBidMessage(auctionEndArgs, cachedAuction) ); }, handleBidTimeout(timeoutBids) { @@ -144,14 +189,34 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER cachedAuction.timeoutBids.push(bid); }); }, + handleBillable(billableArgs) { + const cachedAuction = this.getCachedAuction(billableArgs.auctionId); + /* Filter Greenbids Billable Events only */ + if (billableArgs.vendor === 'greenbidsRtdProvider') { + cachedAuction.billingId = billableArgs.billingId || 'unknown_billing_id'; + } + }, track({eventType, args}) { - switch (eventType) { - case BID_TIMEOUT: - this.handleBidTimeout(args); - break; - case AUCTION_END: - this.handleAuctionEnd(args); - break; + try { + if (eventType === AUCTION_INIT) { + this.handleAuctionInit(args); + } + + if (this.getCachedAuction(args?.auctionId)?.isSampled ?? true) { + switch (eventType) { + case BID_TIMEOUT: + this.handleBidTimeout(args); + break; + case AUCTION_END: + this.handleAuctionEnd(args); + break; + case BILLABLE_EVENT: + this.handleBillable(args); + break; + } + } + } catch (e) { + logWarn('There was an error handling event ' + eventType); } }, getAnalyticsOptions() { diff --git a/modules/greenbidsAnalyticsAdapter.md b/modules/greenbidsAnalyticsAdapter.md index 46e3af2c5e2..1be2c1741ed 100644 --- a/modules/greenbidsAnalyticsAdapter.md +++ b/modules/greenbidsAnalyticsAdapter.md @@ -1,23 +1,24 @@ -# Overview +#### Registration -``` -Module Name: Greenbids Analytics Adapter -Module Type: Analytics Adapter -Maintainer: jb@greenbids.ai -``` +The Greenbids Analytics adapter requires setup and approval from the +Greenbids team. Please reach out to our team for more information [greenbids.ai](https://greenbids.ai). -# Description +#### Analytics Options -Analytics adapter for Greenbids +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-------------|---------|--------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|------------------| +| pbuid | required | The Greenbids Publisher ID | greenbids-publisher-1 | string | +| greenbidsSampling | optional | sampling factor [0-1] (a value of 0.1 will filter 90% of the traffic) | 1.0 | float | -# Test Parameters +### Example Configuration -``` -{ - provider: 'greenbids', - options: { - pbuid: "PBUID_FROM_GREENBIDS" - sampling: 1.0 - } -} -``` +```javascript + pbjs.enableAnalytics({ + provider: 'greenbids', + options: { + pbuid: "greenbids-publisher-1" // please contact Greenbids to get a pbuid for yourself + greenbidsSampling: 1.0 + } + }); +``` \ No newline at end of file diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index b3d79f05996..7fcd163a7c2 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,12 +1,13 @@ -import { logError } from '../src/utils.js'; +import { logError, deepClone, generateUUID, deepSetValue, deepAccess } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; const MODULE_NAME = 'greenbidsRtdProvider'; -const MODULE_VERSION = '1.0.0'; +const MODULE_VERSION = '2.0.0'; const ENDPOINT = 'https://t.greenbids.ai'; -const auctionInfo = {}; const rtdOptions = {}; function init(moduleConfig) { @@ -16,22 +17,33 @@ function init(moduleConfig) { return false; } else { rtdOptions.pbuid = params?.pbuid; - rtdOptions.targetTPR = params?.targetTPR || 0.99; rtdOptions.timeout = params?.timeout || 200; return true; } } function onAuctionInitEvent(auctionDetails) { - auctionInfo.auctionId = auctionDetails.auctionId; + /* Emitting one billing event per auction */ + let defaultId = 'default_id'; + let greenbidsId = deepAccess(auctionDetails.adUnits[0], 'ortb2Imp.ext.greenbids.greenbidsId', defaultId); + /* greenbids was successfully called so we emit the event */ + if (greenbidsId !== defaultId) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: auctionDetails.auctionId, + vendor: MODULE_NAME + }); + } } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - let promise = createPromise(reqBidsConfigObj); + let greenbidsId = generateUUID(); + let promise = createPromise(reqBidsConfigObj, greenbidsId); promise.then(callback); } -function createPromise(reqBidsConfigObj) { +function createPromise(reqBidsConfigObj, greenbidsId) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { resolve(reqBidsConfigObj); @@ -40,7 +52,7 @@ function createPromise(reqBidsConfigObj) { ENDPOINT, { success: (response) => { - processSuccessResponse(response, timeoutId, reqBidsConfigObj); + processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId); resolve(reqBidsConfigObj); }, error: () => { @@ -48,24 +60,35 @@ function createPromise(reqBidsConfigObj) { resolve(reqBidsConfigObj); }, }, - createPayload(reqBidsConfigObj), - { contentType: 'application/json' } + createPayload(reqBidsConfigObj, greenbidsId), + { + contentType: 'application/json', + customHeaders: { + 'Greenbids-Pbuid': rtdOptions.pbuid + } + } ); }); } -function processSuccessResponse(response, timeoutId, reqBidsConfigObj) { +function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) { clearTimeout(timeoutId); const responseAdUnits = JSON.parse(response); - - updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits); + updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); } -function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) { +function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { adUnits.forEach((adUnit) => { const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); if (matchingAdUnit) { - removeFalseBidders(adUnit, matchingAdUnit); + deepSetValue(adUnit, 'ortb2Imp.ext.greenbids', { + greenbidsId: greenbidsId, + keptInAuction: matchingAdUnit.bidders, + isExploration: matchingAdUnit.isExploration + }); + if (!matchingAdUnit.isExploration) { + removeFalseBidders(adUnit, matchingAdUnit); + } } }); } @@ -85,14 +108,24 @@ function getFalseBidders(bidders) { .map(([bidder]) => bidder); } -function createPayload(reqBidsConfigObj) { +function stripAdUnits(adUnits) { + const stripedAdUnits = deepClone(adUnits); + return stripedAdUnits.map(adUnit => { + adUnit.bids = adUnit.bids.map(bid => { + return { bidder: bid.bidder }; + }); + return adUnit; + }); +} + +function createPayload(reqBidsConfigObj, greenbidsId) { return JSON.stringify({ - auctionId: auctionInfo.auctionId, version: MODULE_VERSION, + ...rtdOptions, referrer: window.location.href, prebid: '$prebid.version$', - rtdOptions: rtdOptions, - adUnits: reqBidsConfigObj.adUnits, + greenbidsId: greenbidsId, + adUnits: stripAdUnits(reqBidsConfigObj.adUnits), }); } @@ -105,6 +138,7 @@ export const greenbidsSubmodule = { findMatchingAdUnit: findMatchingAdUnit, removeFalseBidders: removeFalseBidders, getFalseBidders: getFalseBidders, + stripAdUnits: stripAdUnits, }; submodule('realTimeData', greenbidsSubmodule); diff --git a/modules/greenbidsRtdProvider.md b/modules/greenbidsRtdProvider.md index 85b8f5a7859..ab8105a4537 100644 --- a/modules/greenbidsRtdProvider.md +++ b/modules/greenbidsRtdProvider.md @@ -2,6 +2,7 @@ ``` Module Name: Greenbids RTD Provider +Module Version: 2.0.0 Module Type: RTD Provider Maintainer: jb@greenbids.ai ``` @@ -21,7 +22,6 @@ This module is configured as part of the `realTimeData.dataProviders` object. | `waitForIt ` | required (mandatory true value) | Tells prebid auction to wait for the result of this module | `'true'` | `boolean` | | `params` | required | | | `Object` | | `params.pbuid` | required | The client site id provided by Greenbids. | `'TEST_FROM_GREENBIDS'` | `string` | -| `params.targetTPR` | optional (default 0.95) | Target True positive rate for the throttling model | `0.99` | `[0-1]` | | `params.timeout` | optional (default 200) | Maximum amount of milliseconds allowed for module to finish working (has to be <= to the realTimeData.auctionDelay property) | `200` | `number` | #### Example diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 2e44ac46f91..9673633a0fe 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -18,7 +18,7 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'grid'; const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; -const USP_DELETE_DATA_HANDLER = 'https://media.grid.bidswitch.net/uspapi_delete' +const USP_DELETE_DATA_HANDLER = 'https://media.grid.bidswitch.net/uspapi_delete_c2s' const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; @@ -91,7 +91,7 @@ export const spec = { let {bidderRequestId, gdprConsent, uspConsent, timeout, refererInfo, gppConsent} = bidderRequest || {}; const referer = refererInfo ? encodeURIComponent(refererInfo.page) : ''; - const tmax = timeout; + const tmax = parseInt(timeout) || null; const imp = []; const bidsMap = {}; const requests = []; @@ -133,7 +133,7 @@ export const spec = { }; if (ortb2Imp) { if (ortb2Imp.instl) { - impObj.instl = ortb2Imp.instl; + impObj.instl = parseInt(ortb2Imp.instl) || null; } if (ortb2Imp.ext) { @@ -464,16 +464,7 @@ export const spec = { }, onDataDeletionRequest: function(data) { - const uids = []; - const aliases = [spec.code, ...spec.aliases.map((alias) => alias.code || alias)]; - data.forEach(({ bids }) => bids && bids.forEach(({ bidder, params }) => { - if (aliases.includes(bidder) && params && params.uid) { - uids.push(params.uid); - } - })); - if (uids.length) { - spec.ajaxCall(USP_DELETE_DATA_HANDLER, () => {}, JSON.stringify({ uids }), {contentType: 'application/json', method: 'POST'}); - } + spec.ajaxCall(USP_DELETE_DATA_HANDLER, null, null, {method: 'GET'}); } }; @@ -485,7 +476,7 @@ export const spec = { */ function _getFloor (mediaTypes, bid) { const curMediaType = mediaTypes.video ? 'video' : 'banner'; - let floor = bid.params.bidFloor || bid.params.floorcpm || 0; + let floor = parseFloat(bid.params.bidFloor || bid.params.floorcpm || 0) || null; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ @@ -595,8 +586,8 @@ function createVideoRequest(videoParams, mediaType, bidSizes) { if (!videoData.w || !videoData.h) return; - const minDur = mind || durationRangeSec[0] || videoData.minduration; - const maxDur = maxd || durationRangeSec[1] || videoData.maxduration; + const minDur = mind || durationRangeSec[0] || parseInt(videoData.minduration) || null; + const maxDur = maxd || durationRangeSec[1] || parseInt(videoData.maxduration) || null; if (minDur) { videoData.minduration = minDur; diff --git a/modules/growadvertisingBidAdapter.js b/modules/growadvertisingBidAdapter.js index b9b256dbaff..f6f7867f0fe 100644 --- a/modules/growadvertisingBidAdapter.js +++ b/modules/growadvertisingBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getBidIdParameter, deepAccess, _each, triggerPixel } from '../src/utils.js'; +import {deepAccess, _each, triggerPixel, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index e7c572ae48a..5c7cc254f1d 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -13,7 +13,7 @@ import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' -const ENDPOINT_URL = 'https://p2.gcprivacy.com/v3/pb/analytics' +const ENDPOINT_URL = 'https://analytics.gcprivacy.com/v3/pb/analytics' export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index aec49c64fa3..e50a4e73019 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -5,11 +5,12 @@ * @requires module:modules/userId */ -import {logError, logInfo, pick, tryAppendQueryString} from '../src/utils.js'; +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'; const MODULE_NAME = 'growthCodeId'; const GC_DATA_KEY = '_gc_data'; diff --git a/modules/growthCodeRtdProvider.js b/modules/growthCodeRtdProvider.js index 370ace9a203..b12b25a0951 100644 --- a/modules/growthCodeRtdProvider.js +++ b/modules/growthCodeRtdProvider.js @@ -5,10 +5,11 @@ import { submodule } from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; import { - logMessage, logError, tryAppendQueryString, mergeDeep + logMessage, logError, mergeDeep } from '../src/utils.js'; import * as ajax from '../src/ajax.js'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const MODULE_NAME = 'growthCodeRtd'; const LOG_PREFIX = 'GrowthCodeRtd: '; @@ -59,7 +60,11 @@ function init(config, userConsent) { items = tryParse(storage.getDataFromLocalStorage(RTD_CACHE_KEY, null)); - return callServer(configParams, items, expiresAt, userConsent); + if (configParams.pid === undefined) { + return true; // Die gracefully + } else { + return callServer(configParams, items, expiresAt, userConsent); + } } function callServer(configParams, items, expiresAt, userConsent) { // Expire Cache @@ -76,8 +81,8 @@ function callServer(configParams, items, expiresAt, userConsent) { url = tryAppendQueryString(url, 'pid', configParams.pid); url = tryAppendQueryString(url, 'u', window.location.href); url = tryAppendQueryString(url, 'gcid', gcid); - if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentData.getTCData.tcString)) { - url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentData.getTCData.tcString) + if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentString)) { + url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentString) } ajax.ajaxBuilder()(url, { diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index d050af4ac8f..deee906298e 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -14,6 +14,7 @@ const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const TIME_TO_LIVE = 60; const DELAY_REQUEST_TIME = 1800000; // setting to 30 mins +const pubProvidedIdSources = ['dac.co.jp', 'audigent.com', 'id5-sync.com', 'liveramp.com', 'intentiq.com', 'liveintent.com', 'crwdcntrl.net', 'quantcast.com', 'adserver.org', 'yahoo.com'] let invalidRequestIds = {}; let pageViewId = null; @@ -175,6 +176,7 @@ function _getVidParams(attributes) { linearity: li, startdelay: sd, placement: pt, + plcmt, protocols = [], playerSize = [] } = attributes; @@ -186,7 +188,7 @@ function _getVidParams(attributes) { pr = protocols.join(','); } - return { + const result = { mind, maxd, li, @@ -196,6 +198,11 @@ function _getVidParams(attributes) { viw, vih }; + // Add vplcmt property to the result object if plcmt is available + if (plcmt !== undefined && plcmt !== null) { + result.vplcmt = plcmt; + } + return result; } /** @@ -310,7 +317,23 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-174 Removed unnecessary checks to fix failing test data.lt = lt; data.to = to; - + function jsoStringifynWithMaxLength(data, maxLength) { + let jsonString = JSON.stringify(data); + if (jsonString.length <= maxLength) { + return jsonString; + } else { + const truncatedData = data.slice(0, Math.floor(data.length * (maxLength / jsonString.length))); + jsonString = JSON.stringify(truncatedData); + return jsonString; + } + } + // Send filtered pubProvidedId's + if (userId && userId.pubProvidedId) { + let filteredData = userId.pubProvidedId.filter(item => pubProvidedIdSources.includes(item.source)); + let maxLength = 1800; // replace this with your desired maximum length + let truncatedJsonString = jsoStringifynWithMaxLength(filteredData, maxLength); + data.pubProvidedId = truncatedJsonString + } // ADJS-1286 Read id5 id linktype field if (userId && userId.id5id && userId.id5id.uid && userId.id5id.ext) { data.id5Id = userId.id5id.uid || null @@ -322,9 +345,6 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-134 Retrieve ID envelopes for (const eid in eids) data[eid] = eids[eid]; - // ADJS-1024 & ADSS-1297 & ADTS-175 - gpid && (data.gpid = gpid); - if (mediaTypes.banner) { sizes = mediaTypes.banner.sizes; } else if (mediaTypes.video) { @@ -332,6 +352,9 @@ function buildRequests(validBidRequests, bidderRequest) { data = _getVidParams(mediaTypes.video); } + // ADJS-1024 & ADSS-1297 & ADTS-175 + gpid && (data.gpid = gpid); + if (pageViewId) { data.pv = pageViewId; } @@ -383,15 +406,11 @@ function buildRequests(validBidRequests, bidderRequest) { data.uspConsent = uspConsent; } if (gppConsent) { - data.gppConsent = { - gppString: bidderRequest.gppConsent.gppString, - gpp_sid: bidderRequest.gppConsent.applicableSections - } + data.gppString = bidderRequest.gppConsent.gppString ? bidderRequest.gppConsent.gppString : '' + data.gppSid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' } else if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) { - data.gppConsent = { - gppString: bidderRequest.ortb2.regs.gpp, - gpp_sid: bidderRequest.ortb2.regs.gpp_sid - }; + data.gppString = bidderRequest.ortb2.regs.gpp + data.gppSid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid.join(',') : '' } if (coppa) { data.coppa = coppa; diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index c60f0f812a4..4141e9a01f8 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -9,8 +9,11 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isPlainObject, logError, logInfo} from '../src/utils.js'; +import { config } from '../src/config.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +const LOG_PREFIX = '[hadronIdSystem]'; const HADRONID_LOCAL_NAME = 'auHadronId'; const MODULE_NAME = 'hadronId'; const AU_GVLID = 561; @@ -42,6 +45,8 @@ const urlAddParams = (url, params) => { return url + (url.indexOf('?') > -1 ? '&' : '?') + params } +const isDebug = config.getConfig('debug') || false; + /** @type {Submodule} */ export const hadronIdSubmodule = { /** @@ -88,7 +93,7 @@ export const hadronIdSubmodule = { } catch (error) { logError(error); } - logInfo(`Response from backend is ${responseObj}`); + logInfo(LOG_PREFIX, `Response from backend is ${response}`, responseObj); hadronId = responseObj['hadronId']; storage.setDataInLocalStorage(HADRONID_LOCAL_NAME, hadronId); responseObj = {id: {hadronId}}; @@ -100,13 +105,34 @@ export const hadronIdSubmodule = { callback(); } }; - logInfo('HadronId not found in storage, calling backend...'); - const url = urlAddParams( + let url = urlAddParams( // config.params.url and config.params.urlArg are not documented // since their use is for debugging purposes only paramOrDefault(config.params.url, DEFAULT_HADRON_URL_ENDPOINT, config.params.urlArg), - `partner_id=${partnerId}&_it=prebid` + `partner_id=${partnerId}&_it=prebid&t=1&src=id` // src=id => the backend was called from getId ); + if (isDebug) { + url += '&debug=1' + } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent) { + url += `${gdprConsent.consentString ? '&gdprString=' + encodeURIComponent(gdprConsent.consentString) : ''}`; + url += `&gdpr=${gdprConsent.gdprApplies === true ? 1 : 0}`; + } + + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString) { + url += `&us_privacy=${encodeURIComponent(usPrivacyString)}`; + } + + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + url += `${gppConsent.gppString ? '&gpp=' + encodeURIComponent(gppConsent.gppString) : ''}`; + url += `${gppConsent.applicableSections ? '&gpp_sid=' + encodeURIComponent(gppConsent.applicableSections) : ''}`; + } + + logInfo(LOG_PREFIX, `hadronId not found in storage, calling home (${url})`); + ajax(url, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; diff --git a/modules/hadronIdSystem.md b/modules/hadronIdSystem.md index 7521cca06ac..212030cbcd9 100644 --- a/modules/hadronIdSystem.md +++ b/modules/hadronIdSystem.md @@ -35,5 +35,4 @@ The below parameters apply only to the HadronID User ID Module integration. | value | Optional | Object | Used only if the page has a separate mechanism for storing the Hadron ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"hadronId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | | params | Optional | Object | Used to store params for the id system | | params.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | -| params.url | Optional | String | Set an alternate GET url for HadronId with this parameter | -| params.urlArg | Optional | Object | Optional url parameter for params.url | + | diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js index 2073063168d..fbcbb9492c7 100644 --- a/modules/holidBidAdapter.js +++ b/modules/holidBidAdapter.js @@ -1,7 +1,6 @@ import { deepAccess, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, isStr, logMessage, triggerPixel, diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index 994be7c0804..b9de7ef4e46 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -1,7 +1,9 @@ -import { submodule } from '../src/hook.js'; +import {submodule} from '../src/hook.js'; import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -76,7 +78,7 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { if (!utils.isEmpty(adSlot)) { p = adSlot.gptSlot; } else { - if (!utils.isEmpty(adUnitPath) && utils.hasOwn(adUnitPath, bidRequest.code)) { + if (!utils.isEmpty(adUnitPath) && adUnitPath.hasOwnProperty(bidRequest.code)) { if (utils.isStr(adUnitPath[bidRequest.code]) && !utils.isEmpty(adUnitPath[bidRequest.code])) { p = adUnitPath[bidRequest.code]; } @@ -86,13 +88,13 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { } function stringifySlot(bidRequest, adUnitPath) { - const sizes = utils.getAdUnitSizes(bidRequest); + const sizes = getAdUnitSizes(bidRequest); const id = bidRequest.code; const ss = stringifySlotSizes(sizes); - const adSlot = utils.getGptSlotInfoForAdUnitCode(bidRequest.code); + const adSlot = getGptSlotInfoForAdUnitCode(bidRequest.code); const p = getAdUnitPath(adSlot, bidRequest, adUnitPath); const slot = { id, ss, p }; - const keyValues = utils.getKeys(slot).map(function (key) { + const keyValues = Object.keys(slot).map(function (key) { return [key, slot[key]].join(':'); }); return '{' + keyValues.join(',') + '}'; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index e12aea5f8d1..5ed12d8def2 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -19,7 +19,7 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {uspDataHandler} from '../src/adapterManager.js'; +import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'id5Id'; @@ -118,7 +118,7 @@ export const id5IdSubmodule = { } const resp = function (cbFunction) { - new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute() + new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData(), gppDataHandler.getConsentData()).execute() .then(response => { cbFunction(response) }) @@ -170,11 +170,12 @@ export const id5IdSubmodule = { }; class IdFetchFlow { - constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData) { + constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) { this.submoduleConfig = submoduleConfig this.gdprConsentData = gdprConsentData this.cacheIdObj = cacheIdObj this.usPrivacyData = usPrivacyData + this.gppData = gppData } execute() { @@ -285,6 +286,11 @@ class IdFetchFlow { if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) { data.us_privacy = this.usPrivacyData; } + if (this.gppData) { + data.gpp_string = this.gppData.gppString; + data.gpp_sid = this.gppData.applicableSections; + } + if (signature !== undefined && !isEmptyStr(signature)) { data.s = signature; } diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index cf90290b1d8..927fa10f87b 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,6 +1,6 @@ -# ID5 Universal ID +# ID5 ID -The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://support.id5.io/portal/en/kb/articles/prebid-js-user-id-module). +The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/prebid-user-id-module/id5-prebid-user-id-module). ## ID5 ID Registration @@ -29,7 +29,7 @@ pbjs.setConfig({ abTesting: { // optional enabled: true, // false by default controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) - }, + }, disableExtensions: false // optional }, storage: { @@ -49,7 +49,7 @@ pbjs.setConfig({ | name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | | params | Required | Object | Details for the ID5 ID. | | | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | | params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | | params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | | params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | @@ -68,3 +68,6 @@ pbjs.setConfig({ Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of users you would like to set for the control group. The control group is the set of user where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the user was in the control group or not. It's important to note that the control group is user based, and not request based. In other words, from one page view to another, a user will always be in or out of the control group. + +### A Note on Using Multiple Wrappers +If you or your monetization partners are deploying multiple Prebid wrappers on your websites, you should make sure you add the ID5 ID User ID module to *every* wrapper. Only the bidders configured in the Prebid wrapper where the ID5 ID User ID module is installed and configured will be able to pick up the ID5 ID. Bidders from other Prebid instances will not be able to pick up the ID5 ID. diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js index 29dda216fdc..4e51f87cc81 100644 --- a/modules/idWardRtdProvider.js +++ b/modules/idWardRtdProvider.js @@ -28,9 +28,9 @@ function addRealTimeData(ortb2, rtd) { } /** - * Try parsing stringified array of segment IDs. - * @param {String} data - */ + * Try parsing stringified array of segment IDs. + * @param {String} data + */ function tryParse(data) { try { return JSON.parse(data); @@ -41,12 +41,12 @@ function tryParse(data) { } /** - * Real-time data retrieval from ID Ward - * @param {Object} reqBidsConfigObj - * @param {function} onDone - * @param {Object} rtdConfig - * @param {Object} userConsent - */ + * Real-time data retrieval from ID Ward + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + */ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent) { if (rtdConfig && isPlainObject(rtdConfig.params)) { const jsonData = storage.getDataFromLocalStorage(rtdConfig.params.cohortStorageKey) @@ -85,11 +85,11 @@ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent } /** - * Module init - * @param {Object} provider - * @param {Object} userConsent - * @return {boolean} - */ + * Module init + * @param {Object} provider + * @param {Object} userConsent + * @return {boolean} + */ function init(provider, userConsent) { return true; } diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index ba794df1a9c..d70b5680b69 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -10,6 +10,7 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { gppDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = 'identityLink'; @@ -53,18 +54,21 @@ export const identityLinkSubmodule = { } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const gdprConsentString = hasGdpr ? consentData.consentString : ''; - const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { utils.logInfo('identityLink: Consent string is required to call envelope API.'); return; } - const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; + const gppData = gppDataHandler.getConsentData(); + const gppString = gppData && gppData.gppString ? gppData.gppString : false; + const gppSectionId = gppData && gppData.gppString && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== -1 ? gppData.applicableSections[0] : false; + const hasGpp = gppString && gppSectionId; + const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=4&cv=' + gdprConsentString : ''}${hasGpp ? '&gpp=' + gppString + '&gpp_sid=' + gppSectionId : ''}`; let resp; resp = function (callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint - if (window.ats) { + if (window.ats && window.ats.retrieveEnvelope) { utils.logInfo('identityLink: ATS exists!'); window.ats.retrieveEnvelope(function (envelope) { if (envelope) { diff --git a/modules/illuminBidAdapter.js b/modules/illuminBidAdapter.js new file mode 100644 index 00000000000..36ff7bea0a3 --- /dev/null +++ b/modules/illuminBidAdapter.js @@ -0,0 +1,336 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'illumin'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.illumin.com`; +} + +export function extractCID(params) { + return params.cId; +} + +export function extractPID(params) { + return params.pId; +} + +export function extractSubDomain(params) { + return params.subDomain; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.illumin.com/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.illumin.com/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/illuminBidAdapter.md b/modules/illuminBidAdapter.md new file mode 100644 index 00000000000..8ca656230e4 --- /dev/null +++ b/modules/illuminBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Illumin Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** integrations@illumin.com + +# Description + +Module that connects to Illumin's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'illumin', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index 26d49c49f8c..5aa1d4e4dbe 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -50,8 +50,8 @@ function getSegments(segments, moduleConfig) { } /** -* @param {string} bidderName -*/ + * @param {string} bidderName + */ export function getBidderFunction(bidderName) { const biddersFunction = { pubmatic: function (bid, data, moduleConfig) { diff --git a/modules/imdsBidAdapter.js b/modules/imdsBidAdapter.js index d6f3df94409..4cad1d614c5 100644 --- a/modules/imdsBidAdapter.js +++ b/modules/imdsBidAdapter.js @@ -1,10 +1,11 @@ 'use strict'; -import {deepAccess, deepSetValue, getAdUnitSizes, isFn, isPlainObject, logWarn, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepSetValue, isFn, isPlainObject, logWarn, mergeDeep} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BID_SCHEME = 'https://'; const BID_DOMAIN = 'technoratimedia.com'; @@ -223,7 +224,6 @@ export const spec = { }; if (!serverResponse.body || typeof serverResponse.body != 'object') { - logWarn('IMDS: server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); return; } const {id, seatbid: seatbids} = serverResponse.body; @@ -323,7 +323,7 @@ export const spec = { }); } else if (syncOptions.pixelEnabled) { syncs.push({ - type: 'pixel', + type: 'image', url: `${USER_SYNC_PIXEL_URL}?srv=cs&${queryParams.join('&')}` }); } diff --git a/modules/imdsBidAdapter.md b/modules/imdsBidAdapter.md index 15fb407e7ef..2a50868d726 100644 --- a/modules/imdsBidAdapter.md +++ b/modules/imdsBidAdapter.md @@ -11,11 +11,11 @@ Maintainer: eng-demand@imds.tv The iMedia Digital Services adapter requires setup and approval from iMedia Digital Services. Please reach out to your account manager for more information. -### DFP Video Creative -To use video, setup a `VAST redirect` creative within Google AdManager (DFP) with the following VAST tag URL: +### Google Ad Manager Video Creative +To use video, setup a `VAST redirect` creative within Google Ad Manager with the following VAST tag URL: -``` -https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm%%&AUCTION_PRICE=%%PATTERN:hb_pb_synacormedia%% +```text +https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_uuid_imds%%&AUCTION_PRICE=%%PATTERN:hb_pb_imds%% ``` # Test Parameters diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index f2bf9aaddcb..4c0b9801a36 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -1,7 +1,10 @@ -import {deepAccess, deepSetValue, generateUUID} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {ajax} from '../src/ajax.js'; +'use strict'; + +import { deepAccess, deepSetValue, generateUUID } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'impactify'; const BIDDER_ALIAS = ['imp']; @@ -10,58 +13,125 @@ const DEFAULT_VIDEO_WIDTH = 640; const DEFAULT_VIDEO_HEIGHT = 360; const ORIGIN = 'https://sonic.impactify.media'; const LOGGER_URI = 'https://logger.impactify.media'; -const AUCTIONURI = '/bidder'; -const COOKIESYNCURI = '/static/cookie_sync.html'; -const GVLID = 606; -const GETCONFIG = config.getConfig; - -const getDeviceType = () => { - // OpenRTB Device type - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { - return 5; - } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { - return 4; - } - return 2; -}; +const AUCTION_URI = '/bidder'; +const COOKIE_SYNC_URI = '/static/cookie_sync.html'; +const GVL_ID = 606; +const GET_CONFIG = config.getConfig; +export const STORAGE = getStorageManager({ gvlid: GVL_ID, bidderCode: BIDDER_CODE }); +export const STORAGE_KEY = '_im_str' + +/** + * Helpers object + * @type {{getExtParamsFromBid(*): {impactify: {appId}}, createOrtbImpVideoObj(*): {context: string, playerSize: [number,number], id: string, mimes: [string]}, getDeviceType(): (number), createOrtbImpBannerObj(*, *): {format: [], id: string}}} + */ +const helpers = { + getExtParamsFromBid(bid) { + let ext = { + impactify: { + appId: bid.params.appId + }, + }; -const getFloor = (bid) => { - const floorInfo = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: '*', - size: '*' - }); - if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { - return parseFloat(floorInfo.floor); + if (typeof bid.params.format == 'string') { + ext.impactify.format = bid.params.format; + } + + if (typeof bid.params.style == 'string') { + ext.impactify.style = bid.params.style; + } + + if (typeof bid.params.container == 'string') { + ext.impactify.container = bid.params.container; + } + + if (typeof bid.params.size == 'string') { + ext.impactify.size = bid.params.size; + } + + return ext; + }, + + getDeviceType() { + // OpenRTB Device type + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 5; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 4; + } + return 2; + }, + + createOrtbImpBannerObj(bid, size) { + let sizes = size.split('x'); + + return { + id: 'banner-' + bid.bidId, + format: [{ + w: parseInt(sizes[0]), + h: parseInt(sizes[1]) + }] + } + }, + + createOrtbImpVideoObj(bid) { + return { + id: 'video-' + bid.bidId, + playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], + context: 'outstream', + mimes: ['video/mp4'], + } + }, + + getFloor(bid) { + const floorInfo = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + return parseFloat(floorInfo.floor); + } + return null; + }, + + getImStrFromLocalStorage() { + return STORAGE.localStorageIsEnabled(false) ? STORAGE.getDataFromLocalStorage(STORAGE_KEY, false) : ''; } - return null; + } -const createOpenRtbRequest = (validBidRequests, bidderRequest) => { +/** + * Create an OpenRTB formated object from prebid payload + * @param validBidRequests + * @param bidderRequest + * @returns {{cur: string[], validBidRequests, id, source: {tid}, imp: *[]}} + */ +function createOpenRtbRequest(validBidRequests, bidderRequest) { // Create request and set imp bids inside let request = { id: bidderRequest.bidderRequestId, validBidRequests, cur: [DEFAULT_CURRENCY], imp: [], - source: {tid: bidderRequest.ortb2?.source?.tid} + source: { tid: bidderRequest.ortb2?.source?.tid } }; // Get the url parameters const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const checkPrebid = urlParams.get('_checkPrebid'); - // Force impactify debugging parameter + + // Force impactify debugging parameter if present if (checkPrebid != null) { request.test = Number(checkPrebid); } - // Set Schain in request + // Set SChain in request let schain = deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; - // Set eids + // Set Eids let eids = deepAccess(validBidRequests, '0.userIdAsEids'); if (eids && eids.length) { deepSetValue(request, 'user.ext.eids', eids); @@ -73,13 +143,13 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { request.device = { w: window.innerWidth, h: window.innerHeight, - devicetype: getDeviceType(), + devicetype: helpers.getDeviceType(), ua: navigator.userAgent, js: 1, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, language: ((navigator.language || navigator.userLanguage || '').split('-'))[0] || 'en', }; - request.site = {page: bidderRequest.refererInfo.page}; + request.site = { page: bidderRequest.refererInfo.page }; // Handle privacy settings for GDPR/CCPA/COPPA let gdprApplies = 0; @@ -91,9 +161,10 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + this.syncStore.uspConsent = bidderRequest.uspConsent; } - if (GETCONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); + if (GET_CONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); @@ -104,42 +175,47 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { // Create imps with bids validBidRequests.forEach((bid) => { + let bannerObj = deepAccess(bid.mediaTypes, `banner`); + let imp = { id: bid.bidId, bidfloor: bid.params.bidfloor ? bid.params.bidfloor : 0, - ext: { - impactify: { - appId: bid.params.appId, - format: bid.params.format, - style: bid.params.style - }, - }, - video: { - playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], - context: 'outstream', - mimes: ['video/mp4'], - }, + ext: helpers.getExtParamsFromBid(bid) }; - if (bid.params.container) { - imp.ext.impactify.container = bid.params.container; + + if (bannerObj && typeof imp.ext.impactify.size == 'string') { + imp.banner = { + ...helpers.createOrtbImpBannerObj(bid, imp.ext.impactify.size) + } + } else { + imp.video = { + ...helpers.createOrtbImpVideoObj(bid) + } } + if (typeof bid.getFloor === 'function') { - const floor = getFloor(bid); + const floor = helpers.getFloor(bid); if (floor) { imp.bidfloor = floor; } } + request.imp.push(imp); }); return request; -}; +} +/** + * Export BidderSpec type object and register it to Prebid + * @type {{supportedMediaTypes: string[], interpretResponse: ((function(ServerResponse, *): Bid[])|*), code: string, aliases: string[], getUserSyncs: ((function(SyncOptions, ServerResponse[], *, *): UserSync[])|*), buildRequests: (function(*, *): {method: string, data: string, url}), onTimeout: (function(*): boolean), gvlid: number, isBidRequestValid: ((function(BidRequest): (boolean))|*), onBidWon: (function(*): boolean)}} + */ export const spec = { code: BIDDER_CODE, - gvlid: GVLID, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], aliases: BIDDER_ALIAS, + storageAllowed: true, /** * Determines whether or not the given bid request is valid. @@ -148,13 +224,16 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!bid.params.appId || typeof bid.params.appId != 'string' || !bid.params.format || typeof bid.params.format != 'string' || !bid.params.style || typeof bid.params.style != 'string') { + if (typeof bid.params.appId != 'string' || !bid.params.appId) { return false; } - if (bid.params.format != 'screen' && bid.params.format != 'display') { + if (typeof bid.params.format != 'string' || typeof bid.params.style != 'string' || !bid.params.format || !bid.params.style) { return false; } - if (bid.params.style != 'inline' && bid.params.style != 'impact' && bid.params.style != 'static') { + if (bid.params.format !== 'screen' && bid.params.format !== 'display') { + return false; + } + if (bid.params.style !== 'inline' && bid.params.style !== 'impact' && bid.params.style !== 'static') { return false; } @@ -171,11 +250,20 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // Create a clean openRTB request let request = createOpenRtbRequest(validBidRequests, bidderRequest); + const imStr = helpers.getImStrFromLocalStorage(); + const options = {} + + if (imStr) { + options.customHeaders = { + 'x-impact': imStr + }; + } return { method: 'POST', - url: ORIGIN + AUCTIONURI, + url: ORIGIN + AUCTION_URI, data: JSON.stringify(request), + options }; }, @@ -265,16 +353,16 @@ export const spec = { return [{ type: 'iframe', - url: ORIGIN + COOKIESYNCURI + params + url: ORIGIN + COOKIE_SYNC_URI + params }]; }, /** * Register bidder specific code, which will execute if a bid from this bidder won the auction * @param {Bid} The bid that won the auction - */ - onBidWon: function(bid) { - ajax(`${LOGGER_URI}/log/bidder/won`, null, JSON.stringify(bid), { + */ + onBidWon: function (bid) { + ajax(`${LOGGER_URI}/prebid/won`, null, JSON.stringify(bid), { method: 'POST', contentType: 'application/json' }); @@ -285,9 +373,9 @@ export const spec = { /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data - */ - onTimeout: function(data) { - ajax(`${LOGGER_URI}/log/bidder/timeout`, null, JSON.stringify(data[0]), { + */ + onTimeout: function (data) { + ajax(`${LOGGER_URI}/prebid/timeout`, null, JSON.stringify(data[0]), { method: 'POST', contentType: 'application/json' }); diff --git a/modules/impactifyBidAdapter.md b/modules/impactifyBidAdapter.md index 3de9a8cfb84..de3373395dc 100644 --- a/modules/impactifyBidAdapter.md +++ b/modules/impactifyBidAdapter.md @@ -10,14 +10,22 @@ Maintainer: thomas.destefano@impactify.io Module that connects to the Impactify solution. The impactify bidder need 3 parameters: - - appId : This is your unique publisher identifier - - format : This is the ad format needed, can be : screen or display - - style : This is the ad style needed, can be : inline, impact or static +- appId : This is your unique publisher identifier +- format : This is the ad format needed, can be : screen or display (Only for video media type) +- style : This is the ad style needed, can be : inline, impact or static (Only for video media type) + +Note : Impactify adapter need storage access to work properly (Do not forget to set storageAllowed to true). # Test Parameters ``` - var adUnits = [{ - code: 'your-slot-div-id', // This is your slot div id + pbjs.bidderSettings = { + impactify: { + storageAllowed: true // Mandatory + } + }; + + var adUnitsVideo = [{ + code: 'your-slot-div-id-video', // This is your slot div id mediaTypes: { video: { context: 'outstream' @@ -32,4 +40,24 @@ The impactify bidder need 3 parameters: } }] }]; + + var adUnitsBanner = [{ + code: 'your-slot-div-id-banner', // This is your slot div id + mediaTypes: { + banner: { + sizes: [ + [728, 90] + ] + } + }, + bids: [{ + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'display', + size: '728x90', + style: 'static' + } + }] + }]; ``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index b56cc56a186..b563faf52ac 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -182,6 +182,10 @@ export const CONVERTER = ortbConverter({ })(); const bidResponse = buildBidResponse(bid, context); const idExt = deepAccess(bid, `ext.${BIDDER_CODE}`, {}); + // Programmatic guaranteed flag + if (idExt.pg === 1) { + bidResponse.adserverTargeting = { hb_deal_type_improve: 'pg' }; + } Object.assign(bidResponse, { dealId: (typeof idExt.buying_type === 'string' && idExt.buying_type !== 'rtb') ? idExt.line_item_id : undefined, netRevenue: idExt.is_net || false, diff --git a/modules/incrxBidAdapter.js b/modules/incrxBidAdapter.js index d054309ee40..46be7fb75d6 100644 --- a/modules/incrxBidAdapter.js +++ b/modules/incrxBidAdapter.js @@ -12,22 +12,22 @@ export const spec = { supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function (bid) { return !!(bid.params.placementId); }, /** - * Make a server request from the list of BidRequests. - * - * @param validBidRequests - * @param bidderRequest - * @return Array Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param validBidRequests + * @param bidderRequest + * @return Array Info describing the request to the server. + */ buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); @@ -53,11 +53,11 @@ export const spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function (serverResponse) { const response = serverResponse.body; const bids = []; diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index c770ac69dbe..32ec5b482c4 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, generateUUID, logError, isArray} from '../src/utils.js'; +import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {find} from '../src/polyfill.js'; @@ -12,6 +12,35 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days const BID_TTL = 300; // 5 minutes const GVLID = 910; +const isSubarray = (arr, target) => { + if (!isArrayOfNums(arr) || arr.length === 0) { + return false; + } + const targetSet = new Set(target); + return arr.every(el => targetSet.has(el)); +}; + +export const OPTIONAL_VIDEO_PARAMS = { + 'minduration': (value) => isInteger(value), + 'maxduration': (value) => isInteger(value), + 'protocols': (value) => isSubarray(value, [2, 3, 5, 6, 7, 8]), // protocols values supported by Inticator, according to the OpenRTB spec + 'startdelay': (value) => isInteger(value), + 'linearity': (value) => isInteger(value) && [1].includes(value), + 'skip': (value) => isInteger(value) && [1, 0].includes(value), + 'skipmin': (value) => isInteger(value), + 'skipafter': (value) => isInteger(value), + 'sequence': (value) => isInteger(value), + 'battr': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]), + 'maxextended': (value) => isInteger(value), + 'minbitrate': (value) => isInteger(value), + 'maxbitrate': (value) => isInteger(value), + 'playbackmethod': (value) => isSubarray(value, [1, 2, 3, 4]), + 'playbackend': (value) => isInteger(value) && [1, 2, 3].includes(value), + 'delivery': (value) => isSubarray(value, [1, 2, 3]), + 'pos': (value) => isInteger(value) && [0, 1, 2, 3, 4, 5, 6, 7].includes(value), + 'api': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7]), +}; + export const storage = getStorageManager({bidderCode: BIDDER_CODE}); config.setDefaults({ @@ -68,17 +97,48 @@ function buildBanner(bidRequest) { } function buildVideo(bidRequest) { - const w = deepAccess(bidRequest, 'mediaTypes.video.w'); - const h = deepAccess(bidRequest, 'mediaTypes.video.h'); + let w = deepAccess(bidRequest, 'mediaTypes.video.w'); + let h = deepAccess(bidRequest, 'mediaTypes.video.h'); const mimes = deepAccess(bidRequest, 'mediaTypes.video.mimes'); const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3; + const plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt') || undefined; + const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } - return { + const bidRequestVideo = deepAccess(bidRequest, 'mediaTypes.video'); + let optionalParams = {}; + for (const param in OPTIONAL_VIDEO_PARAMS) { + if (bidRequestVideo[param]) { + optionalParams[param] = bidRequestVideo[param]; + } + } + + let videoObj = { placement, mimes, w, h, + ...optionalParams + } + + if (plcmt) { + videoObj['plcmt'] = plcmt; } + return videoObj } function buildImpression(bidRequest) { @@ -106,8 +166,10 @@ function buildImpression(bidRequest) { return imp; } -function buildDevice() { - const deviceConfig = config.getConfig('device'); +function buildDevice(bidRequest) { + const ortb2Data = bidRequest?.ortb2 || {}; + const deviceConfig = ortb2Data?.device || {} + const device = { w: window.innerWidth, h: window.innerHeight, @@ -184,7 +246,7 @@ function buildRequest(validBidRequests, bidderRequest) { page: bidderRequest.refererInfo.page, ref: bidderRequest.refererInfo.ref, }, - device: buildDevice(), + device: buildDevice(bidderRequest), regs: buildRegs(bidderRequest), user: buildUser(validBidRequests[0]), imp: validBidRequests.map((bidRequest) => buildImpression(bidRequest)), @@ -233,7 +295,11 @@ function buildBid(bid, bidderRequest) { meta.advertiserDomains = bid.adomain } - return { + let mediaType = 'banner'; + if (bid.adm && bid.adm.includes(' 0 ? {meta} : {}) }; + + if (mediaType === 'video') { + bidResponse.vastXml = bid.adm; + } + + // Inticator bid adaptor only returns `vastXml` for video bids. No VastUrl or videoCache. + if (!bidResponse.vastUrl && bidResponse.vastXml) { + bidResponse.vastUrl = 'data:text/xml;charset=utf-8;base64,' + window.btoa(bidResponse.vastXml.replace(/\\"/g, '"')); + } + + return bidResponse; } function buildBidSet(seatbid, bidderRequest) { @@ -313,9 +390,26 @@ function validateVideo(bid) { return true; } + let w = deepAccess(bid, 'mediaTypes.video.w'); + let h = deepAccess(bid, 'mediaTypes.video.h'); + const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } const videoSize = [ - deepAccess(bid, 'mediaTypes.video.w'), - deepAccess(bid, 'mediaTypes.video.h'), + w, + h, ]; if ( @@ -339,6 +433,27 @@ function validateVideo(bid) { return false; } + const plcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); + + if (typeof plcmt !== 'undefined' && typeof plcmt !== 'number') { + logError('insticator: video plcmt is not a number'); + return false; + } + + for (const param in OPTIONAL_VIDEO_PARAMS) { + if (video[param]) { + if (!OPTIONAL_VIDEO_PARAMS[param](video[param])) { + logError(`insticator: video ${param} is invalid or not supported by insticator`); + return false + } + } + } + + if (video.minduration && video.maxduration && video.minduration > video.maxduration) { + logError('insticator: video minduration is greater than maxduration'); + return false; + } + return true; } @@ -380,7 +495,6 @@ export const spec = { interpretResponse: function (serverResponse, request) { const bidderRequest = request.bidderRequest; const body = serverResponse.body; - if (!body || body.id !== bidderRequest.bidderRequestId) { logError('insticator: response id does not match bidderRequestId'); return []; diff --git a/modules/integr8BidAdapter.js b/modules/integr8BidAdapter.js index a85e9b0a55c..79c6533d122 100644 --- a/modules/integr8BidAdapter.js +++ b/modules/integr8BidAdapter.js @@ -4,12 +4,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'integr8'; -const ENDPOINT_URL = 'https://integr8.central.gjirafa.tech/bid'; +const DEFAULT_ENDPOINT_URL = 'https://central.sea.integr8.digital/bid'; const DIMENSION_SEPARATOR = 'x'; const SIZE_SEPARATOR = ';'; -const BISKO_ID = 'biskoId'; +const BISKO_ID = 'integr8Id'; const STORAGE_ID = 'bisko-sid'; -const SEGMENTS = 'biskoSegments'; +const SEGMENTS = 'integr8Segments'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { @@ -31,6 +31,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + let deliveryUrl = ''; const storageId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(STORAGE_ID) || '' : ''; const biskoId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(BISKO_ID) || '' : ''; const segments = storage.localStorageIsEnabled() ? JSON.parse(storage.getDataFromLocalStorage(SEGMENTS)) || [] : []; @@ -55,6 +56,9 @@ export const spec = { if (!pageViewGuid) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } if (!Object.keys(data).length && bidRequest.params.data && Object.keys(bidRequest.params.data).length) { data = bidRequest.params.data; } + if (!deliveryUrl && bidRequest.params && typeof bidRequest.params.deliveryUrl === 'string') { + deliveryUrl = bidRequest.params.deliveryUrl; + } return { sizes: generateSizeParam(bidRequest.sizes), @@ -67,6 +71,10 @@ export const spec = { }; }); + if (!deliveryUrl) { + deliveryUrl = DEFAULT_ENDPOINT_URL; + } + let body = { propertyId: propertyId, pageViewGuid: pageViewGuid, @@ -82,7 +90,7 @@ export const spec = { return [{ method: 'POST', - url: ENDPOINT_URL, + url: deliveryUrl, data: body }]; }, @@ -120,11 +128,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/integr8BidAdapter.md b/modules/integr8BidAdapter.md index eadab7acdb3..da52a2164c6 100644 --- a/modules/integr8BidAdapter.md +++ b/modules/integr8BidAdapter.md @@ -3,7 +3,7 @@ Module Name: Integr8 Bidder Adapter Module Type: Bidder Adapter -Maintainer: arditb@gjirafa.com +Maintainer: myhedin@gjirafa.com # Description Integr8 Bidder Adapter for Prebid.js. @@ -23,8 +23,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846835', //Required + propertyId: '105135', //Required + placementId: '846837', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", @@ -48,8 +49,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846830', //Required + propertyId: '105135', //Required + placementId: '846835', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 0c0d1cdef87..1d608d7136b 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -9,7 +9,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 10, + PREBID_VERSION: 11, METHOD: 'GET', INVIBES_VENDOR_ID: 436, USERID_PROVIDERS: ['pubcid', 'pubProvidedId', 'uid2', 'zeotapIdPlus', 'id5id'], @@ -49,14 +49,36 @@ registerBidder(spec); // some state info is required: cookie info, unique user visit id const topWin = getTopMostWindow(); let invibes = topWin.invibes = topWin.invibes || {}; -invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false]; -invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false]; +invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false, false]; +invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false, false]; invibes.placementBids = invibes.placementBids || []; invibes.pushedCids = invibes.pushedCids || {}; let preventPageViewEvent = false; +let isInfiniteScrollPage = false; +let isPlacementRefresh = false; let _customUserSync; let _disableUserSyncs; +function updateInfiniteScrollFlag() { + const { scrollHeight } = document.documentElement; + + if (invibes.originalURL === undefined) { + invibes.originalURL = window.location.href; + return; + } + + if (invibes.originalScrollHeight === undefined) { + invibes.originalScrollHeight = scrollHeight; + return; + } + + const currentURL = window.location.href; + + if (scrollHeight > invibes.originalScrollHeight && invibes.originalURL !== currentURL) { + isInfiniteScrollPage = true; + } +} + function isBidRequestValid(bid) { if (typeof bid.params !== 'object') { return false; @@ -87,10 +109,24 @@ function buildRequest(bidRequests, bidderRequest) { const _placementIds = []; const _adUnitCodes = []; let _customEndpoint, _userId, _domainId; - let _ivAuctionStart = bidderRequest.auctionStart || Date.now(); + let _ivAuctionStart = Date.now(); + window.invibes = window.invibes || {}; + window.invibes.placementIds = window.invibes.placementIds || []; + + if (isInfiniteScrollPage == false) { + updateInfiniteScrollFlag(); + } bidRequests.forEach(function (bidRequest) { bidRequest.startTime = new Date().getTime(); + + if (window.invibes.placementIds.includes(bidRequest.params.placementId)) { + isPlacementRefresh = true; + } + + window.invibes.placementIds.push(bidRequest.params.placementId); + + _placementIds.push(bidRequest.params.placementId); _placementIds.push(bidRequest.params.placementId); _adUnitCodes.push(bidRequest.adUnitCode); _domainId = _domainId || bidRequest.params.domainId; @@ -138,6 +174,8 @@ function buildRequest(bidRequests, bidderRequest) { tc: invibes.gdpr_consent, isLocalStorageEnabled: storage.hasLocalStorage(), preventPageViewEvent: preventPageViewEvent, + isPlacementRefresh: isPlacementRefresh, + isInfiniteScrollPage: isInfiniteScrollPage, }; let lid = readFromLocalStorage('ivbsdid'); @@ -368,7 +406,9 @@ function addMeta(bidModelMeta) { } function generateRandomId() { - return (Math.round(Math.random() * 1e12)).toString(36).substring(0, 10); + return '10000000100040008000100000000000'.replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); } function getDocumentLocation(bidderRequest) { @@ -568,7 +608,7 @@ function readGdprConsent(gdprConsent) { } let legitimateInterests = getLegitimateInterests(gdprConsent.vendorData); - tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, 10); + tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, purposesLength); let invibesVendorId = CONSTANTS.INVIBES_VENDOR_ID.toString(10); let vendorConsents = getVendorConsents(gdprConsent.vendorData); @@ -621,6 +661,10 @@ function tryCopyValueToArray(value, target, length) { function getPurposeConsentsCounter(vendorData) { if (vendorData.purpose && vendorData.purpose.consents) { + if (vendorData.tcfPolicyVersion >= 4) { + return 11; + } + return 10; } diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index eaf20ad3ad3..1188af471a7 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -3,13 +3,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'iprom'; const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; -const VERSION = 'v1.0.2'; +const VERSION = 'v1.0.3'; const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; +const IAB_GVL_ID = 811; export const spec = { code: BIDDER_CODE, + gvlid: IAB_GVL_ID, isBidRequestValid: function ({ bidder, params = {} } = {}) { // id parameter checks if (!params.id) { diff --git a/modules/iqxBidAdapter.js b/modules/iqxBidAdapter.js new file mode 100644 index 00000000000..1bef158c4a2 --- /dev/null +++ b/modules/iqxBidAdapter.js @@ -0,0 +1,207 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +const CUR = 'USD'; +const BIDDER_CODE = 'iqx'; +const ENDPOINT = 'https://pbjs.iqzonertb.live'; + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['iqx'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/iqxBidAdapter.md b/modules/iqxBidAdapter.md new file mode 100644 index 00000000000..c48864c4306 --- /dev/null +++ b/modules/iqxBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: IQX Bidder Adapter +Module Type: IQX Bidder Adapter +Maintainer: it@iqzone.com +``` + +# Description + +Module that connects to iqx.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/ivsBidAdapter.js b/modules/ivsBidAdapter.js index 47685fbbe46..6f4c024f09f 100644 --- a/modules/ivsBidAdapter.js +++ b/modules/ivsBidAdapter.js @@ -1,5 +1,5 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { deepAccess, deepSetValue, getBidIdParameter, logError } from '../src/utils.js'; +import {deepAccess, deepSetValue, getBidIdParameter, logError} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { INSTREAM } from '../src/video.js'; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index d083fb46798..4cc6807b670 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,10 +1,8 @@ import { contains, - convertTypes, deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, inIframe, isArray, isEmpty, @@ -24,6 +22,8 @@ import { find } from '../src/polyfill.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -76,9 +76,12 @@ const SOURCE_RTI_MAPPING = { 'audigent.com': '', // Hadron ID from Audigent, hadronId 'pubcid.org': '', // SharedID, pubcid 'utiq.com': '', // Utiq + 'criteo.com': '', // Criteo + 'euid.eu': '', // EUID 'intimatemerger.com': '', '33across.com': '', 'liveintent.indexexchange.com': '', + 'google.com': '' }; const PROVIDERS = [ 'britepoolid', @@ -89,7 +92,8 @@ const PROVIDERS = [ 'connectid', 'tapadId', 'quantcastId', - 'pubProvidedId' + 'pubProvidedId', + 'pairId' ]; const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration']; // note: protocol/protocols is also reqd const VIDEO_PARAMS_ALLOW_LIST = [ @@ -234,7 +238,10 @@ export function bidToVideoImp(bid) { imp.video = videoParamRef ? deepClone(bid.params.video) : {}; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } setDisplayManager(imp, bid); @@ -324,7 +331,10 @@ export function bidToNativeImp(bid) { }; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } // AdUnit-Specific First Party Data addAdUnitFPD(imp, bid) @@ -344,26 +354,30 @@ function bidToImp(bid, mediaType) { imp.id = bid.bidId; - imp.ext = {}; + if (isExchangeIdConfigured() && deepAccess(bid, `params.externalId`)) { + deepSetValue(imp, 'ext.externalID', bid.params.externalId); + } if (deepAccess(bid, `params.${mediaType}.siteId`) && !isNaN(Number(bid.params[mediaType].siteId))) { switch (mediaType) { case BANNER: - imp.ext.siteID = bid.params.banner.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.banner.siteId.toString()); break; case VIDEO: - imp.ext.siteID = bid.params.video.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.video.siteId.toString()); break; case NATIVE: - imp.ext.siteID = bid.params.native.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.native.siteId.toString()); break; } } else { - imp.ext.siteID = bid.params.siteId.toString(); + if (bid.params.siteId) { + deepSetValue(imp, 'ext.siteID', bid.params.siteId.toString()); + } } // populate imp level sid if (bid.params.hasOwnProperty('id') && (typeof bid.params.id === 'string' || typeof bid.params.id === 'number')) { - imp.ext.sid = String(bid.params.id); + deepSetValue(imp, 'ext.sid', String(bid.params.id)); } return imp; @@ -409,12 +423,12 @@ function _applyFloor(bid, imp, mediaType) { if (moduleFloor) { imp.bidfloor = moduleFloor.floor; imp.bidfloorcur = moduleFloor.currency; - imp.ext.fl = FLOOR_SOURCE.PBJS; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.PBJS); setFloor = true; } else if (adapterFloor) { imp.bidfloor = adapterFloor.floor; imp.bidfloorcur = adapterFloor.currency; - imp.ext.fl = FLOOR_SOURCE.IX; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.IX); setFloor = true; } @@ -521,7 +535,7 @@ function isValidSize(size) { * Determines whether or not the given size object is an element of the size * array. * - * @param {array} sizeArray The size array. + * @param {Array} sizeArray The size array. * @param {object} size The size object. * @return {boolean} True if the size object is an element of the size array, and false * otherwise. @@ -576,7 +590,7 @@ function checkVideoParams(mediaTypeVideoRef, paramsVideoRef) { * Get One size from Size Array * [[250,350]] -> [250, 350] * [250, 350] -> [250, 350] - * @param {array} sizes array of sizes + * @param {Array} sizes array of sizes */ function getFirstSize(sizes = []) { if (isValidSize(sizes)) { @@ -593,7 +607,7 @@ function getFirstSize(sizes = []) { * * @param {number} bidFloor The bidFloor parameter inside bid request config. * @param {number} bidFloorCur The bidFloorCur parameter inside bid request config. - * @return {bool} True if this is a valid bidFloor parameters format, and false + * @return {boolean} True if this is a valid bidFloor parameters format, and false * otherwise. */ function isValidBidFloorParams(bidFloor, bidFloorCur) { @@ -616,7 +630,7 @@ function nativeMediaTypeValid(bid) { * Get bid request object with the associated id. * * @param {*} id Id of the impression. - * @param {array} impressions List of impressions sent in the request. + * @param {Array} impressions List of impressions sent in the request. * @return {object} The impression with the associated id. */ function getBidRequest(id, impressions, validBidRequests) { @@ -634,7 +648,7 @@ function getBidRequest(id, impressions, validBidRequests) { /** * From the userIdAsEids array, filter for the ones our adserver can use, and modify them * for our purposes, e.g. add rtiPartner - * @param {array} allEids userIdAsEids passed in by prebid + * @param {Array} allEids userIdAsEids passed in by prebid * @return {object} contains toSend (eids to send to the adserver) and seenSources (used to filter * identity info from IX Library) */ @@ -662,11 +676,11 @@ function getEidInfo(allEids) { /** * Builds a request object to be sent to the ad server based on bid requests. * - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest An object containing other info like gdprConsent. * @param {object} impressions An object containing a list of impression objects describing the bids for each transaction - * @param {array} version Endpoint version denoting banner, video or native. - * @return {array} List of objects describing the request to the server. + * @param {Array} version Endpoint version denoting banner, video or native. + * @return {Array} List of objects describing the request to the server. * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { @@ -694,7 +708,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = addRequestedFeatureToggles(r, FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES) // getting ixdiags for adunits of the video, outstream & multi format (MF) style - let ixdiag = buildIXDiag(validBidRequests); + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + let ixdiag = buildIXDiag(validBidRequests, fledgeEnabled); for (var key in ixdiag) { r.ext.ixdiag[key] = ixdiag[key]; } @@ -704,8 +719,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = applyRegulations(r, bidderRequest); let payload = {}; - siteID = validBidRequests[0].params.siteId; - payload.s = siteID; + if (validBidRequests[0].params.siteId) { + siteID = validBidRequests[0].params.siteId; + payload.s = siteID; + } const impKeys = Object.keys(impressions); let isFpdAdded = false; @@ -741,9 +758,19 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = removeSiteIDs(r); if (isLastAdUnit) { + let exchangeUrl = `${baseUrl}?`; + + if (siteID !== 0) { + exchangeUrl += `s=${siteID}`; + } + + if (isExchangeIdConfigured()) { + exchangeUrl += siteID !== 0 ? '&' : ''; + exchangeUrl += `p=${config.getConfig('exchangeId')}`; + } requests.push({ method: 'POST', - url: baseUrl + '?s=' + siteID, + url: exchangeUrl, data: deepClone(r), option: { contentType: 'text/plain', @@ -762,8 +789,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { /** * addRTI adds RTI info of the partner to retrieved user IDs from prebid ID module. * - * @param {array} userEids userEids info retrieved from prebid - * @param {array} eidInfo eidInfo info from prebid + * @param {Array} userEids userEids info retrieved from prebid + * @param {Array} eidInfo eidInfo info from prebid */ function addRTI(userEids, eidInfo) { let identityInfo = window.headertag.getIdentityInfo(); @@ -782,7 +809,7 @@ function addRTI(userEids, eidInfo) { /** * createRequest creates the base request object - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @return {object} Object describing the request to the server. */ function createRequest(validBidRequests) { @@ -822,9 +849,9 @@ function addRequestedFeatureToggles(r, requestedFeatureToggles) { * * @param {object} r Base reuqest object. * @param {object} bidderRequest An object containing other info like gdprConsent. - * @param {array} impressions A list of impressions to be added to the request. - * @param {array} validBidRequests A list of valid bid request config objects. - * @param {array} userEids User ID info retrieved from Prebid ID module. + * @param {Array} impressions A list of impressions to be added to the request. + * @param {Array} validBidRequests A list of valid bid request config objects. + * @param {Array} userEids User ID info retrieved from Prebid ID module. * @return {object} Enriched object describing the request to the server. */ function enrichRequest(r, bidderRequest, impressions, validBidRequests, userEids) { @@ -931,10 +958,10 @@ function applyRegulations(r, bidderRequest) { /** * addImpressions adds impressions to request object * - * @param {array} impressions List of impressions to be added to the request. - * @param {array} impKeys List of impression keys. + * @param {Array} impressions List of impressions to be added to the request. + * @param {Array} impKeys List of impression keys. * @param {object} r Reuqest object. - * @param {int} adUnitIndex Index of the current add unit + * @param {number} adUnitIndex Index of the current add unit * @return {object} Reqyest object with added impressions describing the request to the server. */ function addImpressions(impressions, impKeys, r, adUnitIndex) { @@ -950,6 +977,7 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { const dfpAdUnitCode = impressions[impKeys[adUnitIndex]].dfp_ad_unit_code; const tid = impressions[impKeys[adUnitIndex]].tid; const sid = impressions[impKeys[adUnitIndex]].sid; + const auctionEnvironment = impressions[impKeys[adUnitIndex]].ae; const bannerImpressions = impressionObjects.filter(impression => BANNER in impression); const otherImpressions = impressionObjects.filter(impression => !(BANNER in impression)); @@ -964,6 +992,7 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { for (const impId in bannerImpsKeyed) { const bannerImps = bannerImpsKeyed[impId]; const { id, banner: { topframe } } = bannerImps[0]; + let externalID = deepAccess(bannerImps[0], 'ext.externalID'); const _bannerImpression = { id, banner: { @@ -973,15 +1002,24 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { }; for (let i = 0; i < _bannerImpression.banner.format.length; i++) { - // We add sid in imp.ext.sid therefore, remove from banner.format[].ext - if (_bannerImpression.banner.format[i].ext != null && _bannerImpression.banner.format[i].ext.sid != null) { - delete _bannerImpression.banner.format[i].ext.sid; + // We add sid and externalID in imp.ext therefore, remove from banner.format[].ext + if (_bannerImpression.banner.format[i].ext != null) { + if (_bannerImpression.banner.format[i].ext.sid != null) { + delete _bannerImpression.banner.format[i].ext.sid; + } + if (_bannerImpression.banner.format[i].ext.externalID != null) { + delete _bannerImpression.banner.format[i].ext.externalID; + } } // add floor per size if ('bidfloor' in bannerImps[i]) { _bannerImpression.banner.format[i].ext.bidfloor = bannerImps[i].bidfloor; } + + if (JSON.stringify(_bannerImpression.banner.format[i].ext) === '{}') { + delete _bannerImpression.banner.format[i].ext; + } } const position = impressions[impKeys[adUnitIndex]].pos; @@ -989,12 +1027,19 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { _bannerImpression.banner.pos = position; } - if (dfpAdUnitCode || gpid || tid || sid) { + if (dfpAdUnitCode || gpid || tid || sid || auctionEnvironment || externalID) { _bannerImpression.ext = {}; + _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; _bannerImpression.ext.gpid = gpid; _bannerImpression.ext.tid = tid; _bannerImpression.ext.sid = sid; + _bannerImpression.ext.externalID = externalID; + + // enable fledge auction + if (auctionEnvironment == 1) { + _bannerImpression.ext.ae = 1; + } } if ('bidfloor' in bannerImps[0]) { @@ -1018,7 +1063,9 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { // Removes imp.ext.bidfloor // Sets imp.ext.siteID to one of the other [video/native].ext.siteid if imp.ext.siteID doesnt exist otherImpressions.forEach(imp => { - deepSetValue(imp, 'ext.gpid', gpid); + if (gpid) { + deepSetValue(imp, 'ext.gpid', gpid); + } if (r.imp.length > 0) { let matchFound = false; r.imp.forEach((rImp, index) => { @@ -1066,7 +1113,7 @@ This function retrieves the page URL and appends first party data query paramete to it without adding duplicate query parameters. Returns original referer URL if no IX FPD exists. @param {Object} bidderRequest - The bidder request object containing information about the bid and the page. @returns {string} - The modified page URL with first party data query parameters appended. -*/ + */ function getIxFirstPartyDataPageUrl (bidderRequest) { // Parse additional runtime configs. const bidderCode = (bidderRequest && bidderRequest.bidderCode) || 'ix'; @@ -1096,7 +1143,7 @@ This function appends the provided query parameters to the given URL without add @param {string} url - The base URL to which query parameters will be appended. @param {Object} params - An object containing key-value pairs of query parameters to append. @returns {string} - The modified URL with the provided query parameters appended. -*/ + */ function appendIXQueryParams(bidderRequest, url, params) { let urlObj; try { @@ -1181,10 +1228,10 @@ function addAdUnitFPD(imp, bid) { /** * addIdentifiersInfo adds indentifier info to ixDaig. * - * @param {array} impressions List of impressions to be added to the request. + * @param {Array} impressions List of impressions to be added to the request. * @param {object} r Reuqest object. - * @param {array} impKeys List of impression keys. - * @param {int} adUnitIndex Index of the current add unit + * @param {Array} impKeys List of impression keys. + * @param {number} adUnitIndex Index of the current add unit * @param {object} payload Request payload object. * @param {string} baseUrl Base exchagne URL. * @return {object} Reqyest object with added indentigfier info to ixDiag. @@ -1207,7 +1254,7 @@ function addIdentifiersInfo(impressions, r, impKeys, adUnitIndex, payload, baseU /** * Return an object of user IDs stored by Prebid User ID module * - * @returns {array} ID providers that are present in userIds + * @returns {Array} ID providers that are present in userIds */ function _getUserIds(bidRequest) { const userIds = bidRequest.userId || {}; @@ -1218,15 +1265,16 @@ function _getUserIds(bidRequest) { /** * Calculates IX diagnostics values and packages them into an object * - * @param {array} validBidRequests The valid bid requests from prebid + * @param {Array} validBidRequests - The valid bid requests from prebid + * @param {boolean} fledgeEnabled - Flag indicating if protected audience (fledge) is enabled * @return {Object} IX diag values for ad units */ -function buildIXDiag(validBidRequests) { +function buildIXDiag(validBidRequests, fledgeEnabled) { var adUnitMap = validBidRequests .map(bidRequest => bidRequest.adUnitCode) .filter((value, index, arr) => arr.indexOf(value) === index); - var ixdiag = { + let ixdiag = { mfu: 0, bu: 0, iu: 0, @@ -1237,12 +1285,13 @@ function buildIXDiag(validBidRequests) { version: '$prebid.version$', userIds: _getUserIds(validBidRequests[0]), url: window.location.href.split('?')[0], - vpd: defaultVideoPlacement + vpd: defaultVideoPlacement, + ae: fledgeEnabled }; // create ad unit map and collect the required diag properties - for (let i = 0; i < adUnitMap.length; i++) { - var bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnitMap[i])[0]; + for (let adUnit of adUnitMap) { + let bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnit)[0]; if (deepAccess(bid, 'mediaTypes')) { if (Object.keys(bid.mediaTypes).length > 1) { @@ -1278,8 +1327,8 @@ function buildIXDiag(validBidRequests) { /** * - * @param {array} bannerSizeList list of banner sizes - * @param {array} bannerSize the size to be removed + * @param {Array} bannerSizeList list of banner sizes + * @param {Array} bannerSize the size to be removed * @return {boolean} true if successfully removed, false if not found */ @@ -1348,7 +1397,7 @@ function createVideoImps(validBidRequest, videoImps) { * @param {object} missingBannerSizes reference to missing banner config sizes * @param {object} bannerImps reference to created banner impressions */ -function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { +function createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest) { let imp = bidToBannerImp(validBidRequest); const bannerSizeDefined = includesSize(deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), deepAccess(validBidRequest, 'params.size')); @@ -1364,6 +1413,21 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { bannerImps[validBidRequest.adUnitCode].tagId = deepAccess(validBidRequest, 'params.tagId'); bannerImps[validBidRequest.adUnitCode].pos = deepAccess(validBidRequest, 'mediaTypes.banner.pos'); + // Add Fledge flag if enabled + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + if (fledgeEnabled) { + const auctionEnvironment = deepAccess(validBidRequest, 'ortb2Imp.ext.ae') + if (auctionEnvironment) { + if (isInteger(auctionEnvironment)) { + bannerImps[validBidRequest.adUnitCode].ae = auctionEnvironment; + } else { + logWarn('error setting auction environment flag - must be an integer') + } + } else if (deepAccess(bidderRequest, 'defaultForSlots') == 1) { + bannerImps[validBidRequest.adUnitCode].ae = 1 + } + } + // AdUnit-Specific First Party Data const adUnitFPD = deepAccess(validBidRequest, 'ortb2Imp.ext.data'); if (adUnitFPD) { @@ -1423,7 +1487,7 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { /** * @param {object} bid ValidBidRequest object, used to adjust floor * @param {object} imp Impression object to be modified - * @param {array} newSize The new size to be applied + * @param {Array} newSize The new size to be applied * @return {object} newImp Updated impression object */ function createMissingBannerImp(bid, imp, newSize) { @@ -1599,6 +1663,17 @@ function isIndexRendererPreferred(bid) { return !isValid || renderer.backupOnly; } +function isExchangeIdConfigured() { + let exchangeId = config.getConfig('exchangeId'); + if (typeof exchangeId === 'number' && isFinite(exchangeId)) { + return true; + } + if (typeof exchangeId === 'string' && exchangeId.trim() !== '' && isFinite(Number(exchangeId))) { + return true; + } + return false; +} + export const spec = { code: BIDDER_CODE, @@ -1656,14 +1731,21 @@ export const spec = { } } - if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { - logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + if (!isExchangeIdConfigured() && bid.params.siteId == undefined) { + logError('IX Bid Adapter: Invalid configuration - either siteId or exchangeId must be configured.'); return false; } - if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { - logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); - return false; + if (bid.params.siteId !== undefined) { + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { + logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } + + if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { + logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } } if (hasBidFloor || hasBidFloorCur) { @@ -1696,10 +1778,15 @@ export const spec = { return nativeMediaTypeValid(bid); }, + // For testing only - resets the siteID to 0 so that it can be set again + resetSiteID: function () { + siteID = 0; + }, + /** * Make a server request from the list of BidRequests. * - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest A object contains bids and other info like gdprConsent. * @return {object} Info describing the request to the server. */ @@ -1718,7 +1805,7 @@ export const spec = { for (const type in adUnitMediaTypes) { switch (adUnitMediaTypes[type]) { case BANNER: - createBannerImps(validBidRequest, missingBannerSizes, bannerImps); + createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest); break; case VIDEO: createVideoImps(validBidRequest, videoImps) @@ -1787,19 +1874,24 @@ export const spec = { * * @param {object} serverResponse A successful response from the server. * @param {object} bidderRequest The bid request sent to the server. - * @return {array} An array of bids which were nested inside the server. + * @return {Array} An array of bids which were nested inside the server. */ interpretResponse: function (serverResponse, bidderRequest) { const bids = []; let bid = null; - if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { - FEATURE_TOGGLES.setFeatureToggles(serverResponse); + // Extract the FLEDGE auction configuration list from the response + let fledgeAuctionConfigs = deepAccess(serverResponse, 'body.ext.protectedAudienceAuctionConfigs') || []; + + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + + if (!serverResponse.hasOwnProperty('body')) { return bids; } const responseBody = serverResponse.body; - const seatbid = responseBody.seatbid; + const seatbid = responseBody.seatbid || []; + for (let i = 0; i < seatbid.length; i++) { if (!seatbid[i].hasOwnProperty('bid')) { continue; @@ -1835,8 +1927,28 @@ export const spec = { } } - FEATURE_TOGGLES.setFeatureToggles(serverResponse); - return bids; + if (Array.isArray(fledgeAuctionConfigs) && fledgeAuctionConfigs.length > 0) { + // Validate and filter fledgeAuctionConfigs + fledgeAuctionConfigs = fledgeAuctionConfigs.filter(config => { + if (!isValidAuctionConfig(config)) { + logWarn('Malformed auction config detected:', config); + return false; + } + return true; + }); + + try { + return { + bids, + fledgeAuctionConfigs, + }; + } catch (error) { + logWarn('Error attaching AuctionConfigs', error); + return bids; + } + } else { + return bids; + } }, /** @@ -1854,8 +1966,8 @@ export const spec = { /** * Determine which user syncs should occur * @param {object} syncOptions - * @param {array} serverResponses - * @returns {array} User sync pixels + * @param {Array} serverResponses + * @returns {Array} User sync pixels */ getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; @@ -1896,11 +2008,11 @@ export const spec = { }; /** - * Build img user sync url - * @param {int} syncsPerBidder number of syncs Per Bidder - * @param {int} index index to pass - * @returns {string} img user sync url - */ + * Build img user sync url + * @param {number} syncsPerBidder number of syncs Per Bidder + * @param {number} index index to pass + * @returns {string} img user sync url + */ function buildImgSyncUrl(syncsPerBidder, index) { let consentString = ''; let gdprApplies = '0'; @@ -1910,13 +2022,14 @@ function buildImgSyncUrl(syncsPerBidder, index) { if (gdprConsent && gdprConsent.hasOwnProperty('consentString')) { consentString = gdprConsent.consentString || ''; } + let siteIdParam = siteID !== 0 ? '&site_id=' + siteID.toString() : ''; - return IMG_USER_SYNC_URL + '&site_id=' + siteID.toString() + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); + return IMG_USER_SYNC_URL + siteIdParam + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); } /** * Combines all imps into a single object - * @param {array} imps array of imps + * @param {Array} imps array of imps * @returns object */ export function combineImps(imps) { @@ -2057,6 +2170,15 @@ function getFormatCount(imp) { return formatCount; } +/** + * Checks if auction config is valid + * @param {object} config + * @returns bool + */ +function isValidAuctionConfig(config) { + return typeof config === 'object' && config !== null; +} + /** * Adds device.w / device.h info * @param {object} r diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 638cb11c5ab..0705c5932cf 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -469,6 +469,11 @@ pbjs.setConfig({ The timeout value must be a positive whole number in milliseconds. +Protected Audience API (FLEDGE) +=========================== + +In order to enable receiving [Protected Audience API](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) traffic, follow Prebid's documentation on [fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) module to build and enable Fledge. + Additional Information ====================== diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index 824bc3828b4..75268e9d168 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getDNT, isArray, logWarn} from '../src/utils.js'; +import {deepAccess, getDNT, isArray, logWarn, isFn, isPlainObject, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -7,13 +7,36 @@ import {ajax} from '../src/ajax.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; +const ADAPTER_VERSION = '2.1.0'; +const PREBID_VERSION = '$prebid.version$'; + const BIDDER_CODE = 'jixie'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); -const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.media/jxhbrenderer.1.1.min.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; +/** + * Get bid floor from Price Floors Module + * + * @param {Object} bid + * @returns {float||null} + */ +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + /** * Own miscellaneous support functions: */ @@ -38,7 +61,18 @@ function setIds_(clientId, sessionId) { } catch (error) {} } -function fetchIds_() { +/** + * fetch some ids from cookie, LS. + * @returns + */ +const defaultGenIds_ = [ + { id: '_jxtoko' }, + { id: '_jxifo' }, + { id: '_jxtdid' }, + { id: '_jxcomp' } +]; + +function fetchIds_(cfg) { let ret = { client_id_c: '', client_id_ls: '', @@ -56,9 +90,11 @@ function fetchIds_() { if (tmp) ret.client_id_ls = tmp; tmp = storage.getDataFromLocalStorage('_jxxs'); if (tmp) ret.session_id_ls = tmp; - ['_jxtoko', '_jxifo', '_jxtdid', '__uid2_advertising_token'].forEach(function(n) { - tmp = storage.getCookie(n); - if (tmp) ret.jxeids[n] = tmp; + + let arr = cfg.genids ? cfg.genids : defaultGenIds_; + arr.forEach(function(o) { + tmp = storage.getCookie(o.ck ? o.ck : o.id); + if (tmp) ret.jxeids[o.id] = tmp; }); } catch (error) {} return ret; @@ -76,14 +112,6 @@ function getDevice_() { return device; } -function pingTracking_(endpointOverride, qpobj) { - internal.ajax((endpointOverride || EVENTS_URL), null, qpobj, { - withCredentials: true, - method: 'GET', - crossOrigin: true - }); -} - function jxOutstreamRender_(bidAd) { bidAd.renderer.push(() => { window.JixieOutstreamVideo.init({ @@ -145,7 +173,6 @@ export const internal = { export const spec = { code: BIDDER_CODE, - EVENTS_URL: EVENTS_URL, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -171,32 +198,29 @@ export const spec = { params: one.params, gpid: gpid }; + let bidFloor = getBidFloor(one); + if (bidFloor) { + tmp.bidFloor = bidFloor; + } bids.push(tmp); }); + let jxCfg = config.getConfig('jixie') || {}; - let jixieCfgBlob = config.getConfig('jixie'); - if (!jixieCfgBlob) { - jixieCfgBlob = {}; - } - - let ids = fetchIds_(); + let ids = fetchIds_(jxCfg); let eids = []; let miscDims = internal.getMiscDims(); let schain = deepAccess(validBidRequests[0], 'schain'); - let eids1 = validBidRequests[0].userIdAsEids + let eids1 = validBidRequests[0].userIdAsEids; // all available user ids are sent to our backend in the standard array layout: if (eids1 && eids1.length) { eids = eids1; } // we want to send this blob of info to our backend: - let pg = config.getConfig('priceGranularity'); - if (!pg) { - pg = {}; - } let transformedParams = Object.assign({}, { // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionid: bidderRequest.auctionId, + auctionid: bidderRequest.auctionId || '', + aid: jxCfg.aid || '', timeout: bidderRequest.timeout, currency: currency, timestamp: (new Date()).getTime(), @@ -207,8 +231,10 @@ export const spec = { bids: bids, eids: eids, schain: schain, - pricegranularity: pg, - cfg: jixieCfgBlob + pricegranularity: (config.getConfig('priceGranularity') || {}), + ver: ADAPTER_VERSION, + pbjsver: PREBID_VERSION, + cfg: jxCfg }, ids); return Object.assign({}, { method: 'POST', @@ -219,48 +245,20 @@ export const spec = { }, onTimeout: function(timeoutData) { - let jxCfgBlob = config.getConfig('jixie'); - if (jxCfgBlob && jxCfgBlob.onTimeout == 'off') { - return; - } - let url = null;// default - if (jxCfgBlob && jxCfgBlob.onTimeoutUrl && typeof jxCfgBlob.onTimeoutUrl == 'string') { - url = jxCfgBlob.onTimeoutUrl; - } - let miscDims = internal.getMiscDims(); - pingTracking_(url, // no overriding ping URL . just use default - { - action: 'hbtimeout', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - auctionid: deepAccess(timeoutData, '0.auctionId'), - timeout: deepAccess(timeoutData, '0.timeout'), - count: timeoutData.length - }); + logError('jixie adapter timed out for the auction.', timeoutData); }, onBidWon: function(bid) { - if (bid.notrack) { - return; - } if (bid.trackingUrl) { - pingTracking_(bid.trackingUrl, {}); - } else { - let miscDims = internal.getMiscDims(); - pingTracking_((bid.trackingUrlBase ? bid.trackingUrlBase : null), { - action: 'hbbidwon', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - cid: bid.cid, - cpid: bid.cpid, - jxbidid: bid.jxBidId, - auctionid: bid.auctionId, - cpm: bid.cpm, - requestid: bid.requestId + internal.ajax(bid.trackingUrl, null, {}, { + withCredentials: true, + method: 'GET', + crossOrigin: true }); } + logInfo( + `jixie adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}` + ); }, interpretResponse: function(response, bidRequest) { @@ -268,7 +266,6 @@ export const spec = { const bidResponses = []; response.body.bids.forEach(function(oneBid) { let bnd = {}; - Object.assign(bnd, oneBid); if (oneBid.osplayer) { bnd.adResponse = { diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index b79843dccfd..18f89c33ad2 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -26,16 +26,16 @@ let resumeBidRequest; /** @type {RtdSubmodule} */ export const jwplayerSubmodule = { /** - * used to link submodule with realTimeData - * @type {string} - */ + * used to link submodule with realTimeData + * @type {string} + */ name: SUBMODULE_NAME, /** - * add targeting data to bids and signal completion to realTimeData module - * @function - * @param {Obj} bidReqConfig - * @param {function} onDone - */ + * add targeting data to bids and signal completion to realTimeData module + * @function + * @param {Obj} bidReqConfig + * @param {function} onDone + */ getBidRequestData: enrichBidRequest, init }; diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 1dde4453222..9d8c7bc06a1 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -24,6 +24,7 @@ const CURRENCY = Object.freeze({ }); const REQUEST_KEYS = Object.freeze({ + USER_DATA: 'ortb2.user.data', SOCIAL_CANVAS: 'params.socialCanvas', SUA: 'ortb2.device.sua', TDID_ADAPTER: 'userId.tdid', @@ -97,15 +98,26 @@ function buildRequests(validBidRequests, bidderRequest) { user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent), }); + if (firstBidRequest.ortb2 != null) { + krakenParams.site = { + cat: firstBidRequest.ortb2.site.cat + } + } + + // Add schain if (firstBidRequest.schain && firstBidRequest.schain.nodes) { krakenParams.schain = firstBidRequest.schain } + // Add user data object if available + krakenParams.user.data = deepAccess(firstBidRequest, REQUEST_KEYS.USER_DATA) || []; + const reqCount = getRequestCount() if (reqCount != null) { krakenParams.requestCount = reqCount; } + // Add currency if not USD if (currency != null && currency != CURRENCY.US_DOLLAR) { krakenParams.cur = currency; } @@ -463,8 +475,8 @@ function getImpression(bid) { imp.bidderWinCount = bid.bidderWinsCount; } - const gpid = getGPID(bid) - if (gpid != null && gpid != '') { + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { imp.fpd = { gpid: gpid } @@ -487,29 +499,6 @@ function getImpression(bid) { return imp } -function getGPID(bid) { - if (bid.ortb2Imp != null) { - if (bid.ortb2Imp.gpid != null && bid.ortb2Imp.gpid != '') { - return bid.ortb2Imp.gpid; - } - - if (bid.ortb2Imp.ext != null && bid.ortb2Imp.ext.data != null) { - if (bid.ortb2Imp.ext.data.pbAdSlot != null && bid.ortb2Imp.ext.data.pbAdSlot != '') { - return bid.ortb2Imp.ext.data.pbAdSlot; - } - - if (bid.ortb2Imp.ext.data.adServer != null && bid.ortb2Imp.ext.data.adServer.adSlot != null && bid.ortb2Imp.ext.data.adServer.adSlot != '') { - return bid.ortb2Imp.ext.data.adServer.adSlot; - } - } - } - - if (bid.adUnitCode != null && bid.adUnitCode != '') { - return bid.adUnitCode; - } - return ''; -} - export const spec = { gvlid: BIDDER.GVLID, code: BIDDER.CODE, diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 0a868661310..5a5536e0c1a 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js deleted file mode 100644 index fb3f6e4e231..00000000000 --- a/modules/kulturemediaBidAdapter.js +++ /dev/null @@ -1,472 +0,0 @@ -import { - deepAccess, - deepSetValue, - isArray, - isFn, - isNumber, - isPlainObject, - isStr, - logError, - logInfo, - logMessage -} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; - -const BIDDER_CODE = 'kulturemedia'; -const DEFAULT_BID_TTL = 300; -const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_NET_REVENUE = true; -const DEFAULT_NETWORK_ID = 1; -const OPENRTB_VIDEO_PARAMS = [ - 'mimes', - 'minduration', - 'maxduration', - 'placement', - 'protocols', - 'startdelay', - 'skip', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackmethod', - 'api', - 'linearity' -]; - -export const spec = { - code: BIDDER_CODE, - VERSION: '1.0.0', - supportedMediaTypes: [BANNER, VIDEO], - ENDPOINT: 'https://ads.kulture.media/pbjs', - - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bidRequest The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return ( - _validateParams(bid) && - _validateBanner(bid) && - _validateVideo(bid) - ); - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. - * @param {BidderRequest} bidderRequest bidder request object. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function (validBidRequests, bidderRequest) { - if (!validBidRequests || !bidderRequest) { - return; - } - - // We need to refactor this to support mixed content when there are both - // banner and video bid requests - let openrtbRequest; - if (hasBannerMediaType(validBidRequests[0])) { - openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest); - } else if (hasVideoMediaType(validBidRequests[0])) { - openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest); - } - - // adding schain object - if (validBidRequests[0].schain) { - deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); - } - - // Attaching GDPR Consent Params - if (bidderRequest.gdprConsent) { - deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); - } - - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - // EIDS - const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); - if (Array.isArray(eids) && eids.length > 0) { - deepSetValue(openrtbRequest, 'user.ext.eids', eids); - } - - let publisherId = validBidRequests[0].params.publisherId; - let placementId = validBidRequests[0].params.placementId; - const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID; - - if (validBidRequests[0].params.e2etest) { - logMessage('E2E test mode enabled'); - publisherId = 'e2etest' - } - let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; - - if (placementId) { - baseEndpoint += '&placementId=' + placementId - } - if (networkId) { - baseEndpoint += '&nId=' + networkId - } - - const payloadString = JSON.stringify(openrtbRequest); - return { - method: 'POST', - url: baseEndpoint, - data: payloadString, - }; - }, - - interpretResponse: function (serverResponse) { - const bidResponses = []; - const response = (serverResponse || {}).body; - // response is always one seat (exchange) with (optional) bids for each impression - if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { - response.seatbid[0].bid.forEach(bid => { - if (bid.adm && bid.price) { - bidResponses.push(_createBidResponse(bid)); - } - }) - } else { - logInfo('kulturemedia.interpretResponse :: no valid responses to interpret'); - } - return bidResponses; - }, - - getUserSyncs: function (syncOptions, serverResponses) { - logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); - let syncs = []; - - if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { - return syncs; - } - - serverResponses.forEach(resp => { - const userSync = deepAccess(resp, 'body.ext.usersync'); - if (userSync) { - let syncDetails = []; - Object.keys(userSync).forEach(key => { - const value = userSync[key]; - if (value.syncs && value.syncs.length) { - syncDetails = syncDetails.concat(value.syncs); - } - }); - syncDetails.forEach(syncDetails => { - syncs.push({ - type: syncDetails.type === 'iframe' ? 'iframe' : 'image', - url: syncDetails.url - }); - }); - - if (!syncOptions.iframeEnabled) { - syncs = syncs.filter(s => s.type !== 'iframe') - } - if (!syncOptions.pixelEnabled) { - syncs = syncs.filter(s => s.type !== 'image') - } - } - }); - logInfo('kulturemedia.getUserSyncs result=%o', syncs); - return syncs; - }, - -}; - -/* ======================================= - * Util Functions - *======================================= */ - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasBannerMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.banner'); -} - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasVideoMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.video'); -} - -function _validateParams(bidRequest) { - if (!bidRequest.params) { - return false; - } - - if (bidRequest.params.e2etest) { - return true; - } - - if (!bidRequest.params.publisherId) { - logError('Validation failed: publisherId not declared'); - return false; - } - - if (!bidRequest.params.placementId) { - logError('Validation failed: placementId not declared'); - return false; - } - - const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); - if (!mediaTypesExists) { - return false; - } - - return true; -} - -/** - * Validates banner bid request. If it is not banner media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateBanner(bidRequest) { - // If there's no banner no need to validate - if (!hasBannerMediaType(bidRequest)) { - return true; - } - const banner = deepAccess(bidRequest, 'mediaTypes.banner'); - if (!Array.isArray(banner.sizes)) { - return false; - } - - return true; -} - -/** - * Validates video bid request. If it is not video media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateVideo(bidRequest) { - // If there's no video no need to validate - if (!hasVideoMediaType(bidRequest)) { - return true; - } - - const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - const params = deepAccess(bidRequest, 'params', {}); - - if (params && params.e2etest) { - return true; - } - - const videoParams = { - ...videoPlacement, - ...videoBidderParams // Bidder Specific overrides - }; - - if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { - logError('Validation failed: mimes are invalid'); - return false; - } - - if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { - logError('Validation failed: protocols are invalid'); - return false; - } - - if (!videoParams.context) { - logError('Validation failed: context id not declared'); - return false; - } - - if (videoParams.context !== 'instream') { - logError('Validation failed: only context instream is supported '); - return false; - } - - if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { - logError('Validation failed: player size not declared or is not in format [[w,h]]'); - return false; - } - - return true; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildVideoRequestData(bidRequest, bidderRequest) { - const {params} = bidRequest; - - const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - - const videoParams = { - ...videoAdUnit, - ...videoBidderParams // Bidder Specific overrides - }; - - if (bidRequest.params && bidRequest.params.e2etest) { - videoParams.playerSize = [[640, 480]] - videoParams.conext = 'instream' - } - - const video = { - w: parseInt(videoParams.playerSize[0][0], 10), - h: parseInt(videoParams.playerSize[0][1], 10), - } - - // Obtain all ORTB params related video from Ad Unit - OPENRTB_VIDEO_PARAMS.forEach((param) => { - if (videoParams.hasOwnProperty(param)) { - video[param] = videoParams[param]; - } - }); - - // Placement Inference Rules: - // - If no placement is defined then default to 1 (In Stream) - video.placement = video.placement || 2; - - // - If product is instream (for instream context) then override placement to 1 - if (params.context === 'instream') { - video.startdelay = video.startdelay || 0; - video.placement = 1; - } - - // bid floor - const bidFloorRequest = { - currency: bidRequest.params.cur || 'USD', - mediaType: 'video', - size: '*' - }; - let floorData = bidRequest.params - if (isFn(bidRequest.getFloor)) { - floorData = bidRequest.getFloor(bidFloorRequest); - } else { - if (params.bidfloor) { - floorData = {floor: params.bidfloor, currency: params.currency || 'USD'}; - } - } - - const openrtbRequest = { - id: bidRequest.bidId, - imp: [ - { - id: '1', - video: video, - secure: isSecure() ? 1 : 0, - bidfloor: floorData.floor, - bidfloorcur: floorData.currency - } - ], - site: { - domain: bidderRequest.refererInfo.domain, - page: bidderRequest.refererInfo.page, - ref: bidderRequest.refererInfo.ref, - }, - ext: { - hb: 1, - prebidver: '$prebid.version$', - adapterver: spec.VERSION, - }, - }; - - // content - if (videoParams.content && isPlainObject(videoParams.content)) { - openrtbRequest.site.content = {}; - const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url']; - const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; - const contentArrayKeys = ['cat']; - const contentObjectKeys = ['ext']; - for (const contentKey in videoBidderParams.content) { - if ( - (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) || - (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) || - (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) || - (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) && - videoParams.content[contentKey].every(catStr => isStr(catStr)))) { - openrtbRequest.site.content[contentKey] = videoParams.content[contentKey]; - } else { - logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); - } - } - } - - return openrtbRequest; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildBannerRequestData(bidRequests, bidderRequest) { - const impr = bidRequests.map(bidRequest => ({ - id: bidRequest.bidId, - banner: { - format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ - w: sizeArr[0], - h: sizeArr[1] - })) - }, - ext: { - exchange: { - placementId: bidRequest.params.placementId - } - } - })); - - const openrtbRequest = { - id: bidderRequest.bidderRequestId, - imp: impr, - site: { - domain: bidderRequest.refererInfo?.domain, - page: bidderRequest.refererInfo?.page, - ref: bidderRequest.refererInfo?.ref, - }, - ext: {} - }; - return openrtbRequest; -} - -function _createBidResponse(bid) { - const isADomainPresent = - bid.adomain && bid.adomain.length; - const bidResponse = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - ad: bid.adm, - ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, - creativeId: bid.crid, - netRevenue: DEFAULT_NET_REVENUE, - currency: DEFAULT_CURRENCY, - mediaType: deepAccess(bid, 'ext.prebid.type', BANNER) - } - - if (isADomainPresent) { - bidResponse.meta = { - advertiserDomains: bid.adomain - }; - } - - if (bidResponse.mediaType === VIDEO) { - bidResponse.vastXml = bid.adm; - } - - return bidResponse; -} - -function isSecure() { - return document.location.protocol === 'https:'; -} - -registerBidder(spec); diff --git a/modules/lemmaDigitalBidAdapter.js b/modules/lemmaDigitalBidAdapter.js index 9fa3081a47e..8a3b05b7ed3 100644 --- a/modules/lemmaDigitalBidAdapter.js +++ b/modules/lemmaDigitalBidAdapter.js @@ -26,7 +26,7 @@ export var spec = { * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. - **/ + */ isBidRequestValid: (bid) => { if (!bid || !bid.params) { utils.logError(LOG_WARN_PREFIX, 'nil/empty bid object'); @@ -51,11 +51,11 @@ export var spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - **/ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: (validBidRequests, bidderRequest) => { if (validBidRequests.length === 0) { return; @@ -79,11 +79,11 @@ export var spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} response A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - **/ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} response A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: (response, request) => { return spec._parseRTBResponse(request, response.body); }, @@ -93,7 +93,7 @@ export var spec = { * @param {SyncOptions} syncOptions Which user syncs are allowed? * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. - **/ + */ getUserSyncs: (syncOptions, serverResponses) => { let syncurl = USER_SYNC + 'pid=' + pubId; if (syncOptions.iframeEnabled) { @@ -115,7 +115,7 @@ export var spec = { /** * parse object - **/ + */ _parseJSON: function (rawPayload) { try { if (rawPayload) { @@ -155,7 +155,7 @@ export var spec = { /** * create IAB standard OpenRTB bid request - **/ + */ _createoRTBRequest: (bidRequests, conf) => { var oRTBObject = {}; try { @@ -202,7 +202,7 @@ export var spec = { /** * create impression array objects - **/ + */ _getImpressionArray: (request) => { var impArray = []; var map = request.map(bid => spec._getImpressionObject(bid)); @@ -218,7 +218,7 @@ export var spec = { /** * create impression (single) object - **/ + */ _getImpressionObject: (bid) => { var impression = {}; var bObj; @@ -277,8 +277,8 @@ export var spec = { }, /** - * set bid floor - **/ + * set bid floor + */ _setFloor: (impObj, bid) => { let bidFloor = -1; // get lowest floor from floorModule @@ -304,8 +304,8 @@ export var spec = { }, /** - * parse Open RTB response - **/ + * parse Open RTB response + */ _parseRTBResponse: (request, response) => { var bidResponses = []; try { @@ -358,8 +358,8 @@ export var spec = { }, /** - * get bid request api end point url - **/ + * get bid request api end point url + */ _endPointURL: (request) => { var params = request && request[0].params ? request[0].params : null; if (params) { @@ -371,8 +371,8 @@ export var spec = { }, /** - * get domain name from url - **/ + * get domain name from url + */ _getDomain: (url) => { var a = document.createElement('a'); a.setAttribute('href', url); @@ -380,8 +380,8 @@ export var spec = { }, /** - * create the site object - **/ + * create the site object + */ _getSiteObject: (request, conf) => { var params = request && request.params ? request.params : null; if (params) { @@ -406,8 +406,8 @@ export var spec = { }, /** - * create the app object - **/ + * create the app object + */ _getAppObject: (request) => { var params = request && request.params ? request.params : null; if (params) { @@ -432,8 +432,8 @@ export var spec = { }, /** - * create the device object - **/ + * create the device object + */ _getDeviceObject: (request) => { var params = request && request.params ? request.params : null; if (params) { @@ -481,8 +481,8 @@ export var spec = { }, /** - * get request ad sizes - **/ + * get request ad sizes + */ _getSizes: (request) => { if (request && request.sizes && utils.isArray(request.sizes[0]) && request.sizes[0].length > 0) { return request.sizes[0]; @@ -491,8 +491,8 @@ export var spec = { }, /** - * create the banner object - **/ + * create the banner object + */ _getBannerRequest: (bid) => { var bObj; var adFormat = []; @@ -531,8 +531,8 @@ export var spec = { }, /** - * create the video object - **/ + * create the video object + */ _getVideoRequest: (bid) => { var vObj; if (utils.deepAccess(bid, 'mediaTypes.video')) { @@ -554,8 +554,8 @@ export var spec = { }, /** - * check media type - **/ + * check media type + */ _checkMediaType: (adm, newBid) => { // Create a regex here to check the strings var videoRegex = new RegExp(/VAST.*version/); diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 2d9e6b63a35..8dc7102091d 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -8,10 +8,12 @@ import { triggerPixel, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; +const DEFAULT_AJAX_TIMEOUT = 5000 const EVENTS_TOPIC = 'pre_lips' const MODULE_NAME = 'liveIntentId'; const LI_PROVIDER_DOMAIN = 'liveintent.com'; @@ -65,6 +67,7 @@ function parseLiveIntentCollectorConfig(collectConfig) { collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); + config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; return config; } @@ -99,9 +102,8 @@ function initializeLiveConnect(configParams) { if (configParams.url) { identityResolutionConfig.url = configParams.url } - if (configParams.ajaxTimeout) { - identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout; - } + + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); @@ -113,6 +115,7 @@ function initializeLiveConnect(configParams) { } liveConnectConfig.wrapperName = 'prebid'; + liveConnectConfig.trackerVersion = '$prebid.version$'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; liveConnectConfig.fireEventDelay = configParams.fireEventDelay; @@ -125,7 +128,11 @@ function initializeLiveConnect(configParams) { liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; liveConnectConfig.gdprConsent = gdprConsent.consentString; } - + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + liveConnectConfig.gppString = gppConsent.gppString; + liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; + } // The second param is the storage object, LS & Cookie manipulation uses PBJS // The third param is the ajax and pixel object, the ajax and pixel use PBJS liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); @@ -150,7 +157,7 @@ function tryFireEvent() { /** @type {Submodule} */ export const liveIntentIdSubmodule = { - moduleMode: process.env.LiveConnectMode, + moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', /** * used to link submodule with config * @type {string} @@ -209,6 +216,18 @@ export const liveIntentIdSubmodule = { result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } } + if (value.openx) { + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.pubmatic) { + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.sovrn) { + result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } + } + return result } @@ -248,6 +267,7 @@ export const liveIntentIdSubmodule = { return { callback: result }; }, eids: { + ...UID2_EIDS, 'lipb': { getValue: function(data) { return data.lipbid; @@ -309,6 +329,42 @@ export const liveIntentIdSubmodule = { return data.ext; } } + }, + 'openx': { + source: 'openx.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'pubmatic': { + source: 'pubmatic.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'sovrn': { + source: 'liveintent.sovrn.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } } } }; diff --git a/modules/lm_kiviadsBidAdapter.js b/modules/lm_kiviadsBidAdapter.js new file mode 100644 index 00000000000..9ba26052727 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.js @@ -0,0 +1,207 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +const CUR = 'USD'; +const BIDDER_CODE = 'lm_kiviads'; +const ENDPOINT = 'https://pbjs.kiviads.live'; + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['kivi'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/lm_kiviadsBidAdapter.md b/modules/lm_kiviadsBidAdapter.md new file mode 100644 index 00000000000..fc1b05d1ef7 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: lm_kiviads Bidder Adapter +Module Type: lm_kiviads Bidder Adapter +Maintainer: pavlo@xe.works +``` + +# Description + +Module that connects to kiviads.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/lockerdomeBidAdapter.js b/modules/lockerdomeBidAdapter.js index 5c38753c1e2..5038eadce30 100644 --- a/modules/lockerdomeBidAdapter.js +++ b/modules/lockerdomeBidAdapter.js @@ -1,6 +1,6 @@ -import { getBidIdParameter } from '../src/utils.js'; import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; export const spec = { code: 'lockerdome', diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index b501e3bef5a..b9665b93494 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -142,6 +142,10 @@ const sendEvent = payload => { ...getTopLevelDetails(), ...payload } + if (window.pbjs?.rp?.eventDispatcher) { + const analyticsEvent = new CustomEvent('beforeSendingMagniteAnalytics', { detail: event }); + window.pbjs.rp.eventDispatcher.dispatchEvent(analyticsEvent); + } ajax( endpoint, null, diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 5ac50936ed6..1d50ad4e4c1 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -124,11 +124,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/mediabramaBidAdapter.js b/modules/mediabramaBidAdapter.js new file mode 100644 index 00000000000..caf6854fe03 --- /dev/null +++ b/modules/mediabramaBidAdapter.js @@ -0,0 +1,155 @@ +import { + isFn, + isStr, + deepAccess, + getWindowTop, + triggerPixel +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'mediabrama'; +const AD_URL = 'https://prebid.mediabrama.com/pbjs'; +const SYNC_URL = 'https://prebid.mediabrama.com/sync'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency || !bid.meta) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + default: + return false; + } +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placementId); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = getWindowTop(); + const location = winTop.location; + const placements = []; + + const request = { + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + host: location.host, + page: location.pathname, + placements: placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent; + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + bidfloor: getBidFloor(bid) + }; + + if (typeof bid.userId !== 'undefined') { + placement.userId = bid.userId; + } + + const mediaType = bid.mediaTypes; + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.adFormat = BANNER; + } + + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidWon: (bid) => { + const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); + triggerPixel(bid.nurl); + } + } +}; + +registerBidder(spec); diff --git a/modules/mediabramaBidAdapter.md b/modules/mediabramaBidAdapter.md new file mode 100644 index 00000000000..fde0a399852 --- /dev/null +++ b/modules/mediabramaBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaBrama Bidder Adapter +Module Type: MediaBrama Bidder Adapter +Maintainer: support@mediabrama.com +``` + +# Description + +Module that connects to mediabrama demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'div-prebid', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mediabrama', + params: { + placementId: '24428' //test, please replace after test + } + } + ] + }, + ]; +``` diff --git a/modules/mediafilterRtdProvider.js b/modules/mediafilterRtdProvider.js new file mode 100644 index 00000000000..8a082ad4d59 --- /dev/null +++ b/modules/mediafilterRtdProvider.js @@ -0,0 +1,94 @@ +/** + * This module adds the Media Filter real-time ad monitoring and protection module. + * + * The {@link module:modules/realTimeData} module is required + * + * For more information, visit {@link https://www.themediatrust.com The Media Trust}. + * + * @author Mirnes Cajlakovic + * @module modules/mediafilterRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { logError, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +/** The event type for Media Filter. */ +export const MEDIAFILTER_EVENT_TYPE = 'com.mediatrust.pbjs.'; +/** The base URL for Media Filter scripts. */ +export const MEDIAFILTER_BASE_URL = 'https://scripts.webcontentassessor.com/scripts/'; + +export const MediaFilter = { + /** + * Registers the Media Filter as a submodule of real-time data. + */ + register: function() { + submodule('realTimeData', { + 'name': 'mediafilter', + 'init': this.generateInitHandler() + }); + }, + + /** + * Sets up the Media Filter by initializing event listeners and loading the external script. + * @param {object} configuration - The configuration object. + */ + setup: function(configuration) { + this.setupEventListener(configuration.configurationHash); + this.setupScript(configuration.configurationHash); + }, + + /** + * Sets up an event listener for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + */ + setupEventListener: function(configurationHash) { + window.addEventListener('message', this.generateEventHandler(configurationHash)); + }, + + /** + * Loads the Media Filter script based on the provided configuration hash. + * @param {string} configurationHash - The configuration hash. + */ + setupScript: function(configurationHash) { + loadExternalScript(MEDIAFILTER_BASE_URL.concat(configurationHash), 'mediafilter', () => {}); + }, + + /** + * Generates an event handler for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + * @returns {function} The generated event handler. + */ + generateEventHandler: function(configurationHash) { + return (windowEvent) => { + if (windowEvent.data.type === MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash)) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': generateUUID(), + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + }); + } + }; + }, + + /** + * Generates an initialization handler for Media Filter. + * @returns {function} The generated init handler. + */ + generateInitHandler: function() { + return (configuration) => { + try { + this.setup(configuration); + } catch (error) { + logError(`Error in initialization: ${error.message}`); + } + }; + } +}; + +// Register the module +MediaFilter.register(); diff --git a/modules/mediafilterRtdProvider.md b/modules/mediafilterRtdProvider.md new file mode 100644 index 00000000000..469479f8d0b --- /dev/null +++ b/modules/mediafilterRtdProvider.md @@ -0,0 +1,37 @@ +## Overview + +**Module:** The Media Filter +**Type: **Real Time Data Module + +As malvertising, scams, and controversial and offensive ad content proliferate across the digital media ecosystem, publishers need advanced controls to both shield audiences from malware attacks and ensure quality site experience. With the market’s fastest and most comprehensive real-time ad quality tool, The Media Trust empowers publisher Ad/Revenue Operations teams to block a wide range of malware, high-risk ad platforms, heavy ads, ads with sensitive or objectionable content, and custom lists (e.g., competitors). Customizable replacement code calls for a new ad to ensure impressions are still monetized. + +[![IMAGE ALT TEXT](http://img.youtube.com/vi/VBHRiirge7s/0.jpg)](http://www.youtube.com/watch?v=VBHRiirge7s "Publishers' Ultimate Avenger: Media Filter") + +To start using this module, please contact [The Media Trust](https://mediatrust.com/how-we-help/media-filter/ "The Media Trust") to get a script and configuration hash for module configuration. + +## Integration + +1. Build Prebid bundle with The Media Filter module included. + +``` +gulp build --modules=mediafilterRtdProvider +``` + +2. Inlcude the bundled script in your application. + +## Configuration + +Add configuration entry to `realTimeData.dataProviders` for The Media Filter module. + +``` +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'mediafilter', + params: { + configurationHash: '', + } + }] + } +}); +``` diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index 98179c49e0d..1fdd3530fae 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -1,14 +1,8 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -38,7 +32,10 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const BIDDER_CODE = 'mediafuse'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -959,7 +956,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -985,7 +982,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 756e636572d..a9c670f35d8 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -10,17 +10,21 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'mediago'; // const PROTOCOL = window.document.location.protocol; -const ENDPOINT_URL = - // ((PROTOCOL === 'https:') ? 'https' : 'http') + - 'https://rtb-us.mediago.io/api/bid?tn='; +const ENDPOINT_URL = 'https://gbid.mediago.io/api/bid?tn='; +const COOKY_SYNC_URL = 'https://gtrace.mediago.io/ju/cs/eplist'; +const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; + const TIME_TO_LIVE = 500; +const GVLID = 1020; // const ENDPOINT_URL = '/api/bid?tn='; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +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; +let reqTimes = 0; /** * čŽˇå–į”¨æˆˇid @@ -31,7 +35,7 @@ const getUserID = () => { if (i === null) { const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid); + storage.setCookie(COOKIE_KEY_MGUID, uuid, STORE_MAX_AGE); return uuid; } return i; @@ -72,7 +76,7 @@ function isMobileAndTablet() { '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', - '|windows ce|xda|xiino|android|ipad|playbook|silk', + '|windows ce|xda|xiino|android|ipad|playbook|silk' ].join(''), 'i' ); @@ -96,7 +100,7 @@ function isMobileAndTablet() { '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', - '|your|zeto|zte-', + '|your|zeto|zte-' ].join(''), 'i' ); @@ -138,7 +142,7 @@ function getBidFloor(bid) { const bidFloor = bid.getFloor({ currency: 'USD', mediaType: '*', - size: '*', + size: '*' }); return bidFloor.floor; } catch (_) { @@ -156,11 +160,7 @@ function transformSizes(requestSizes) { let sizes = []; let sizeObj = {}; - if ( - utils.isArray(requestSizes) && - requestSizes.length === 2 && - !utils.isArray(requestSizes[0]) - ) { + if (utils.isArray(requestSizes) && requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { sizeObj.width = parseInt(requestSizes[0], 10); sizeObj.height = parseInt(requestSizes[1], 10); sizes.push(sizeObj); @@ -187,7 +187,7 @@ const mediagoAdSize = [ { w: 160, h: 600 }, { w: 320, h: 180 }, { w: 320, h: 100 }, - { w: 336, h: 280 }, + { w: 336, h: 280 } ]; /** @@ -207,24 +207,32 @@ function getItems(validBidRequests, bidderRequest) { // įĄŽčŽ¤å°ē寸是åĻįŦĻ合我äģŦčĻæą‚ for (let size of sizes) { - matchSize = mediagoAdSize.find( - (item) => size.width === item.w && size.height === item.h - ); + matchSize = mediagoAdSize.find(item => size.width === item.w && size.height === item.h); if (matchSize) { break; } } if (!matchSize) { - matchSize = sizes[0] - ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } - : { h: 0, w: 0 }; + matchSize = sizes[0] ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } : { h: 0, w: 0 }; } const bidFloor = getBidFloor(req); - // const gpid = - // utils.deepAccess(req, 'ortb2Imp.ext.gpid') || - // utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || - // utils.deepAccess(req, 'params.placementId', 0); + const gpid = + utils.deepAccess(req, 'ortb2Imp.ext.gpid') || + utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || + utils.deepAccess(req, 'params.placementId', 0); + + const gdprConsent = {}; + if (bidderRequest && bidderRequest.gdprConsent) { + gdprConsent.consent = bidderRequest.gdprConsent.consentString; + gdprConsent.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + // if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + // let ac = bidderRequest.gdprConsent.addtlConsent; + // // pull only the ids from the string (after the ~) and convert them to an array of ints + // let acStr = ac.substring(ac.indexOf('~') + 1); + // gdpr_consent.addtl_consent = acStr.split('.').map(id => parseInt(id, 10)); + // } + } // if (mediaTypes.native) {} // banneråšŋ告įąģ型 @@ -237,16 +245,21 @@ function getItems(validBidRequests, bidderRequest) { h: matchSize.h, w: matchSize.w, pos: 1, - format: sizes, + format: sizes }, ext: { - // gpid: gpid, // 加å…Ĩ后无æŗ•čŋ”回åšŋ告 + adUnitCode: req.adUnitCode, + referrer: getReferrer(req, bidderRequest), + ortb2Imp: utils.deepAccess(req, 'ortb2Imp'), // äŧ å…ĨåŽŒæ•´å¯ščąĄīŧŒåˆ†æžæ—Ĩåŋ—数捎 + gpid: gpid, // 加å…Ĩ后无æŗ•čŋ”回åšŋ告 + adslot: utils.deepAccess(req, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + ...gdprConsent // gdpr }, - tagid: req.params && req.params.tagid, + tagid: req.params && req.params.tagid }; itemMaps[id] = { req, - ret, + ret }; } @@ -255,6 +268,21 @@ function getItems(validBidRequests, bidderRequest) { return items; } +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + /** * čŽˇå–rtbč¯ˇæą‚å‚æ•° * @@ -267,7 +295,14 @@ function getParam(validBidRequests, bidderRequest) { const sharedid = utils.deepAccess(validBidRequests[0], 'userId.sharedid.id') || utils.deepAccess(validBidRequests[0], 'userId.pubcid'); - const eids = validBidRequests[0].userIdAsEids || validBidRequests[0].userId; + + const bidsUserIdAsEids = validBidRequests[0].userIdAsEids; + const bidsUserid = validBidRequests[0].userId; + const eids = bidsUserIdAsEids || bidsUserid; + const ppuid = bidsUserid && bidsUserid.pubProvidedId; + const content = utils.deepAccess(bidderRequest, 'ortb2.site.content'); + const cat = utils.deepAccess(bidderRequest, 'ortb2.site.cat'); + reqTimes += 1; let isMobile = isMobileAndTablet() ? 1 : 0; // input test status by Publisher. more frequently for test true req @@ -275,8 +310,7 @@ function getParam(validBidRequests, bidderRequest) { let auctionId = getProperty(bidderRequest, 'auctionId'); let items = getItems(validBidRequests, bidderRequest); - const domain = - utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; + const domain = utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; const location = utils.deepAccess(bidderRequest, 'refererInfo.location'); const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); @@ -300,15 +334,21 @@ function getParam(validBidRequests, bidderRequest) { // ua: 'Mozilla/5.0 (Linux; Android 12; SM-G970U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36', os: navigator.platform || '', ua: navigator.userAgent, - language: /en/.test(navigator.language) ? 'en' : navigator.language, + language: /en/.test(navigator.language) ? 'en' : navigator.language }, ext: { eids, + bidsUserIdAsEids, + bidsUserid, + ppuid, firstPartyData, + content, + cat, + reqTimes }, user: { buyeruid: getUserID(), - id: sharedid || pubcid, + id: sharedid || pubcid }, eids, site: { @@ -321,11 +361,11 @@ function getParam(validBidRequests, bidderRequest) { publisher: { // todo id: domain, - name: domain, - }, + name: domain + } }, imp: items, - tmax: timeout, + tmax: timeout }; return c; } else { @@ -335,6 +375,7 @@ function getParam(validBidRequests, bidderRequest) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, // aliases: ['ex'], // short code /** * Determines whether or not the given bid request is valid. @@ -366,7 +407,7 @@ export const spec = { return { method: 'POST', url: ENDPOINT_URL + globals['token'], - data: payloadString, + data: payloadString }; }, @@ -397,7 +438,7 @@ export const spec = { ttl: TIME_TO_LIVE, // referrer: REFERER, ad: getProperty(bid, 'adm'), - nurl: getProperty(bid, 'nurl'), + nurl: getProperty(bid, 'nurl') // adserverTargeting: { // granularityMultiplier: 0.1, // priceGranularity: 'pbHg', @@ -414,6 +455,38 @@ export const spec = { return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + } else { + return [ + { + type: 'image', + url: `${COOKY_SYNC_URL}?${syncParamUrl}` + } + ]; + } + }, + /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data @@ -433,7 +506,7 @@ export const spec = { if (bid['nurl']) { utils.triggerPixel(bid['nurl']); } - }, + } /** * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index 659da0c16fb..041db71cd34 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, deepAccess, - getGptSlotInfoForAdUnitCode, getWindowTop, isArray, isEmpty, @@ -18,6 +17,7 @@ import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'medianet'; const TRUSTEDSTACK_CODE = 'trustedstack'; diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js index 378a804487a..aee5f6230b2 100644 --- a/modules/mediasniperBidAdapter.js +++ b/modules/mediasniperBidAdapter.js @@ -1,8 +1,7 @@ import { deepAccess, deepClone, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, inIframe, isArray, isEmpty, diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 87404b7a9ff..fb580d81b94 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -20,20 +20,20 @@ export const spec = { aliases: ['msq'], // short code supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { return !!(bid.params.owner && bid.params.code); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -96,11 +96,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { const serverBody = serverResponse.body; // const headerValue = serverResponse.headers.get('some-response-header'); @@ -125,7 +125,7 @@ export const spec = { 'advertiserDomains': value['adomain'] } }; - let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment']; + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; paramsToSearchFor.forEach(param => { if (param in value) { bidResponse['mediasquare'][param] = value[param]; @@ -148,12 +148,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { @@ -164,9 +164,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction - */ + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid if (bid.hasOwnProperty('mediaType') && bid.mediaType == 'video') { @@ -174,7 +174,7 @@ export const spec = { } let params = { pbjs: '$prebid.version$', referer: encodeURIComponent(getRefererInfo().page || getRefererInfo().topmostLocation) }; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment']; + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; if (bid.hasOwnProperty('mediasquare')) { paramsToSearchFor.forEach(param => { if (bid['mediasquare'].hasOwnProperty(param)) { diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index fc77c7cc97d..20c6f817dca 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -87,17 +87,17 @@ function generateId(configParams, configStorage) { /** @type {Submodule} */ export const merkleIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{eids:arrayofields}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{eids:arrayofields}} + */ decode(value) { // Legacy support for a single id const id = (value && value.pam_id && typeof value.pam_id.id === 'string') ? value.pam_id : undefined; @@ -115,12 +115,12 @@ export const merkleIdSubmodule = { }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ getId(config, consentData) { logInfo('User ID - merkleId generating id'); diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 8e889261e52..1e158236deb 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -9,11 +9,10 @@ import { isEmpty, triggerPixel, logWarn, - getBidIdParameter, isFn, isNumber, isBoolean, - isInteger, deepSetValue, + isInteger, deepSetValue, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; diff --git a/modules/mgidXBidAdapter.js b/modules/mgidXBidAdapter.js index 5789f0d8b95..7e084977cf1 100644 --- a/modules/mgidXBidAdapter.js +++ b/modules/mgidXBidAdapter.js @@ -166,10 +166,15 @@ export const spec = { placements, coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, tmax: config.getConfig('bidderTimeout') }; + if (bidderRequest.gdprConsent) { + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index bb0bb76bdbc..a4158e2abfa 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -7,7 +19,7 @@ const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'minutemedia'; const ADAPTER_VERSION = '6.0.0'; const TTL = 360; -const CURRENCY = 'USD'; +const DEFAULT_CURRENCY = 'USD'; const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/'; const MODES = { PRODUCTION: 'hb-mm-multi', @@ -60,7 +72,7 @@ export const spec = { const bidResponse = { requestId: adUnit.requestId, cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, + currency: adUnit.currency || DEFAULT_CURRENCY, width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, @@ -129,16 +141,16 @@ registerBidder(spec); * @param bid {bid} * @returns {Number} */ -function getFloor(bid, mediaType) { +function getFloor(bid, mediaType, currency) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ - currency: CURRENCY, + currency: currency, mediaType: mediaType, size: '*' }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; } /** @@ -274,6 +286,7 @@ function generateBidParameters(bid, bidderRequest) { const {params} = bid; const mediaType = isBanner(bid) ? BANNER : VIDEO; const sizesArray = getSizesArray(bid, mediaType); + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; // fix floor price in case of NAN if (isNaN(params.floorPrice)) { @@ -284,12 +297,13 @@ function generateBidParameters(bid, bidderRequest) { mediaType, adUnitCode: getBidIdParameter('adUnitCode', bid), sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), bidId: getBidIdParameter('bidId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), transactionId: bid.ortb2Imp?.ext?.tid || '', - coppa: 0 + coppa: 0, }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); diff --git a/modules/minutemediaBidAdapter.md b/modules/minutemediaBidAdapter.md index 66b54adaf0e..fdfdf1b32bf 100644 --- a/modules/minutemediaBidAdapter.md +++ b/modules/minutemediaBidAdapter.md @@ -24,6 +24,7 @@ The adapter supports Video(instream) & Banner. | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 | `placementId` | optional | String | A unique placement identifier | "12345678" | `testMode` | optional | Boolean | This activates the test mode | false +| `currency` | optional | String | 3 letters currency | "EUR" # Test Parameters ```javascript diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 33fa6857e85..5e68f56ed1c 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -1,12 +1,39 @@ -import { buildUrl, formatQS, logInfo, triggerPixel } from '../src/utils.js'; +import { + buildUrl, + formatQS, + isFn, + logInfo, + safeJSONParse, + triggerPixel, +} from '../src/utils.js'; +import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; 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 }); + +/* Get Floor price information */ +function getFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return {}; + } + + const bidFloors = bidRequest.getFloor({ + currency: 'USD', + mediaType: BANNER, + }); + + if (!isNaN(bidFloors.floor)) { + return bidFloors; + } +} + export const spec = { aliases: ['msna'], code: BIDDER_CODE, @@ -30,6 +57,18 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + const capKey = `missena.missena.capper.remove-bubble.${validBidRequests[0]?.params.apiKey}`; + const capping = safeJSONParse(storage.getDataFromLocalStorage(capKey)); + const referer = bidderRequest?.refererInfo?.topmostLocation; + if ( + typeof capping?.expiry === 'number' && + new Date().getTime() < capping?.expiry && + (!capping?.referer || capping?.referer == referer) + ) { + logInfo('Missena - Capped'); + return []; + } + return validBidRequests.map((bidRequest) => { const payload = { adunit: bidRequest.adUnitCode, @@ -61,6 +100,12 @@ export const spec = { payload.is_internal = bidRequest.params.isInternal; } payload.userEids = bidRequest.userIdAsEids || []; + + const bidFloor = getFloor(bidRequest); + payload.floor = bidFloor?.floor; + payload.floor_currency = bidFloor?.currency; + payload.currency = config.getConfig('currency.adServerCurrency') || 'EUR'; + return { method: 'POST', url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), @@ -89,7 +134,7 @@ export const spec = { syncOptions, serverResponses, gdprConsent, - uspConsent + uspConsent, ) { if (!syncOptions.iframeEnabled) { return []; @@ -128,8 +173,13 @@ export const spec = { protocol: 'https', hostname, pathname: '/v1/bidsuccess', - search: { t: bid.params[0].apiKey, provider: bid.meta?.networkName, cpm: bid.cpm, currency: bid.currency }, - }) + search: { + t: bid.params[0].apiKey, + provider: bid.meta?.networkName, + cpm: bid.originalCpm, + currency: bid.originalCurrency, + }, + }), ); logInfo('Missena - Bid won', bid); }, diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js index 9ff50e2531f..35e9b03c031 100644 --- a/modules/mobfoxpbBidAdapter.js +++ b/modules/mobfoxpbBidAdapter.js @@ -71,6 +71,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/multibid/index.js b/modules/multibid/index.js index df77a157bee..27b88d47cf7 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -45,10 +45,10 @@ config.getConfig(MODULE_NAME, conf => { }); /** - * @summary validates multibid configuration entries - * @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] - * @return {Boolean} -*/ + * @summary validates multibid configuration entries + * @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] + * @return {Boolean} + */ export function validateMultibid(conf) { let check = true; let duplicate = conf.filter(entry => { @@ -77,10 +77,10 @@ export function validateMultibid(conf) { } /** - * @summary addBidderRequests before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {Object[]} array containing copy of each bidderRequest object -*/ + * @summary addBidderRequests before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {Object[]} array containing copy of each bidderRequest object + */ export function adjustBidderRequestsHook(fn, bidderRequests) { bidderRequests.map(bidRequest => { // Loop through bidderRequests and check if bidderCode exists in multiconfig @@ -95,11 +95,11 @@ export function adjustBidderRequestsHook(fn, bidderRequests) { } /** - * @summary addBidResponse before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {String} ad unit code for bid - * @param {Object} bid object -*/ + * @summary addBidResponse before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {String} ad unit code for bid + * @param {Object} bid object + */ export const addBidResponseHook = timedBidResponseHook('multibid', function addBidResponseHook(fn, adUnitCode, bid, reject) { let floor = deepAccess(bid, 'floorData.floorValue'); @@ -146,9 +146,9 @@ export const addBidResponseHook = timedBidResponseHook('multibid', function addB }); /** -* A descending sort function that will sort the list of objects based on the following: -* - bids without dynamic aliases are sorted before bids with dynamic aliases -*/ + * A descending sort function that will sort the list of objects based on the following: + * - bids without dynamic aliases are sorted before bids with dynamic aliases + */ export function sortByMultibid(a, b) { if (a.bidder !== a.bidderCode && b.bidder === b.bidderCode) { return 1; @@ -162,13 +162,13 @@ export function sortByMultibid(a, b) { } /** - * @summary getHighestCpmBidsFromBidPool before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {Object[]} array of objects containing all bids from bid pool - * @param {Function} function to reduce to only highest cpm value for each bidderCode - * @param {Number} adUnit bidder targeting limit, default set to 0 - * @param {Boolean} default set to false, this hook modifies targeting and sets to true -*/ + * @summary getHighestCpmBidsFromBidPool before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {Object[]} array of objects containing all bids from bid pool + * @param {Function} function to reduce to only highest cpm value for each bidderCode + * @param {Number} adUnit bidder targeting limit, default set to 0 + * @param {Boolean} default set to false, this hook modifies targeting and sets to true + */ export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { if (!config.getConfig('multibid')) resetMultiConfig(); if (hasMultibid) { @@ -216,18 +216,18 @@ export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBi } /** -* Resets globally stored multibid configuration -*/ + * Resets globally stored multibid configuration + */ export const resetMultiConfig = () => { hasMultibid = false; multiConfig = {}; }; /** -* Resets globally stored multibid ad unit bids -*/ + * Resets globally stored multibid ad unit bids + */ export const resetMultibidUnits = () => multibidUnits = {}; /** -* Set up hooks on init -*/ + * Set up hooks on init + */ function init() { // TODO: does this reset logic make sense - what about simultaneous auctions? events.on(CONSTANTS.EVENTS.AUCTION_INIT, resetMultibidUnits); diff --git a/modules/mwOpenLinkIdSystem.js b/modules/mwOpenLinkIdSystem.js index ff23547224b..8cc5cd8bb4a 100644 --- a/modules/mwOpenLinkIdSystem.js +++ b/modules/mwOpenLinkIdSystem.js @@ -112,27 +112,27 @@ export { writeCookie }; /** @type {Submodule} */ export const mwOpenLinkIdSubModule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: 'mwOpenLinkId', /** - * decode the stored id value for passing to bid requests - * @function - * @param {MwOlId} mwOlId - * @return {(Object|undefined} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {MwOlId} mwOlId + * @return {(Object|undefined} + */ decode(mwOlId) { const id = mwOlId && isPlainObject(mwOlId) ? mwOlId.eid : undefined; return id ? { 'mwOpenLinkId': id } : undefined; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleParams} [submoduleParams] - * @returns {id:MwOlId | undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [submoduleParams] + * @returns {id:MwOlId | undefined} + */ getId(submoduleConfig) { const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; if (!isValidConfig(submoduleConfigParams)) return undefined; diff --git a/modules/mygaruIdSystem.js b/modules/mygaruIdSystem.js new file mode 100644 index 00000000000..4d50de16d48 --- /dev/null +++ b/modules/mygaruIdSystem.js @@ -0,0 +1,98 @@ +/** + * This module adds MyGaru Real Time User Sync to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/mygaruIdSystem + * @requires module:modules/userId + */ + +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; + +const bidderCode = 'mygaruId'; +const syncUrl = 'https://ident.mygaru.com/v2/id'; + +export function buildUrl(opts) { + const queryPairs = []; + for (let key in opts) { + if (opts[key] !== undefined) { + queryPairs.push(`${key}=${encodeURIComponent(opts[key])}`); + } + } + return `${syncUrl}?${queryPairs.join('&')}`; +} + +function requestRemoteIdAsync(url) { + return new Promise((resolve) => { + ajax( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { iuid } = jsonResponse; + resolve(iuid); + } catch (e) { + resolve(); + } + }, + error: () => { + resolve(); + }, + }, + undefined, + { + method: 'GET', + contentType: 'application/json' + } + ); + }); +} + +/** @type {Submodule} */ +export const mygaruIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: bidderCode, + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{id: string} | null} + */ + decode(id) { + return id; + }, + /** + * get the MyGaru Id from local storages and initiate a new user sync + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {{id: string | undefined}} + */ + getId(config, consentData) { + const gdprApplies = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies ? 1 : 0; + const gdprConsentString = gdprApplies ? consentData.consentString : undefined; + const url = buildUrl({ + gdprApplies, + gdprConsentString + }); + + return { + url, + callback: function (done) { + return requestRemoteIdAsync(url).then((id) => { + done({ mygaruId: id }); + }) + } + } + }, + eids: { + 'mygaruId': { + source: 'mygaru.com', + atype: 1 + }, + } +}; + +submodule('userId', mygaruIdSubmodule); diff --git a/modules/mygaruIdSystem.md b/modules/mygaruIdSystem.md new file mode 100644 index 00000000000..92724f99469 --- /dev/null +++ b/modules/mygaruIdSystem.md @@ -0,0 +1,24 @@ +## Mygaru User ID Submodule + +MyGaru provides single use tokens as a UserId for SSPs and DSP that consume telecom DMP data. + +## Building Prebid with Mygaru ID Support + +First, make sure to add submodule to your Prebid.js package with: + +``` +gulp build --modules=userId,mygaruIdSystem +``` +Params configuration is not required. +Also mygaru is async, in order to get ids for initial ad auctions you need to add auctionDelay param to userSync config. + +```javascript +pbjs.setConfig({ + userSync: { + auctionDelay: 100, + userIds: [{ + name: 'mygaruId', + }] + } +}); +``` diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 8a472259873..f82bf4002ca 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -74,16 +74,16 @@ function readnavIDFromCookie() { /** @type {Submodule} */ export const naveggIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * decode the stored id value for passing to bid requests - * @function - * @param { Object | string | undefined } value - * @return { Object | string | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { Object | string | undefined } value + * @return { Object | string | undefined } + */ decode(value) { const naveggIdVal = value ? isStr(value) ? value : isPlainObject(value) ? value.id : undefined : undefined; return naveggIdVal ? { @@ -91,11 +91,11 @@ export const naveggIdSubmodule = { } : undefined; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} config - * @return {{id: string | undefined } | undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined } | undefined} + */ getId() { const naveggIdString = readnaveggIdFromLocalStorage() || readnaveggIDFromCookie() || getNaveggIdFromApi() || readoldnaveggIDFromCookie() || readnvgIDFromCookie() || readnavIDFromCookie(); diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 00a3c59b4a6..7c594e2a1c3 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -10,14 +10,14 @@ const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2 const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1' const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2' -function init(config = {}, userConsent) { - config.params = config.params || {} +function init(config, userConsent) { + // config.params = config.params || {} // ignore module if publicToken is missing (module setup failure) - if (!config.params.publicToken) { + if (!config || !config.params || !config.params.publicToken) { logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken') return false; } - if (!config.params.apiUrl) { + if (!config || !config.params || !config.params.apiUrl) { logError('apiUrl missing', 'NeuwoRTDModule', 'config.params.apiUrl') return false; } @@ -25,14 +25,14 @@ function init(config = {}, userConsent) { } export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - config.params = config.params || {}; + const confParams = config.params || {}; logInfo('NeuwoRTDModule', 'starting getBidRequestData') - const wrappedArgUrl = encodeURIComponent(config.params.argUrl || getRefererInfo().page); + const wrappedArgUrl = encodeURIComponent(confParams.argUrl || getRefererInfo().page); /* adjust for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') */ - const joiner = config.params.apiUrl.indexOf('?') < 0 ? '?' : '&' - const url = config.params.apiUrl + joiner + [ - 'token=' + config.params.publicToken, + const joiner = confParams.apiUrl.indexOf('?') < 0 ? '?' : '&' + const url = confParams.apiUrl + joiner + [ + 'token=' + confParams.publicToken, 'url=' + wrappedArgUrl ].join('&') const billingId = generateUUID(); @@ -71,7 +71,7 @@ export function addFragment(base, path, addition) { /** * Concatenate a base array and an array within an object * non-array bases will be arrays, non-arrays at object key will be discarded - * @param {array} base base array to add to + * @param {Array} base base array to add to * @param {object} source object to get an array from * @param {string} key dot-notated path to array within object * @returns base + source[key] if that's an array diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index 2adead66d4e..fb52734d451 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -33,6 +33,8 @@ pbjs.setConfig({realTimeData: { dataProviders: [ neuwoDataProvider ]}}) # Testing +`gulp test --modules=rtdModule,neuwoRtdProvider` + ## Add development tools if necessary - Install node for npm diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index cb660ad9fd6..5dea4f9b651 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -2,6 +2,7 @@ import { _each, createTrackPixelHtml, deepAccess, + deepSetValue, getBidIdParameter, getDefinedParams, getWindowTop, @@ -13,6 +14,7 @@ import { triggerPixel, } from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; import CONSTANTS from '../src/constants.json'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -21,17 +23,33 @@ import * as events from '../src/events.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; +const NM_VERSION = '3.1.0'; +const GVLID = 1060; const BIDDER_CODE = 'nextMillennium'; const ENDPOINT = 'https://pbs.nextmillmedia.com/openrtb2/auction'; const TEST_ENDPOINT = 'https://test.pbs.nextmillmedia.com/openrtb2/auction'; -const SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?'; +const SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}'; const REPORT_ENDPOINT = 'https://report2.hb.brainlyads.com/statistics/metric'; const TIME_TO_LIVE = 360; + const VIDEO_PARAMS = [ - 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', - 'playbackmethod', 'protocols', 'startdelay' + 'api', + 'linearity', + 'maxduration', + 'mimes', + 'minduration', + 'placement', + 'playbackmethod', + 'protocols', + 'startdelay', +]; + +const ALLOWED_ORTB2_PARAMETERS = [ + 'site.pagecat', + 'site.content.cat', + 'site.content.language', + 'device.sua', ]; -const GVLID = 1060; const sendingDataStatistic = initSendingDataStatistic(); events.on(CONSTANTS.EVENTS.AUCTION_INIT, auctionInitHandler); @@ -62,85 +80,37 @@ export const spec = { const id = getPlacementId(bid); const auctionId = bid.auctionId; const bidId = bid.bidId; - let sizes = bid.sizes; - if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; const site = getSiteObj(); const device = getDeviceObj(); const postBody = { - 'id': bidderRequest?.bidderRequestId, - 'ext': { - 'prebid': { - 'storedrequest': { - 'id': id - } + id: bidderRequest?.bidderRequestId, + ext: { + prebid: { + storedrequest: { + id, + }, }, - 'nextMillennium': { - 'refresh_count': window.nmmRefreshCounts[bid.adUnitCode]++, - 'elOffsets': getBoundingClient(bid), - 'scrollTop': window.pageYOffset || document.documentElement.scrollTop - } + nextMillennium: { + nm_version: NM_VERSION, + pbjs_version: getGlobal()?.version || undefined, + refresh_count: window.nmmRefreshCounts[bid.adUnitCode]++, + elOffsets: getBoundingClient(bid), + scrollTop: window.pageYOffset || document.documentElement.scrollTop, + }, }, device, site, - imp: [] - }; - - const imp = { - id: bid.adUnitCode, - ext: { - prebid: { - storedrequest: {id} - } - } - }; - - if (deepAccess(bid, 'mediaTypes.banner')) { - imp.banner = { - format: (sizes || []).map(s => { return {w: s[0], h: s[1]} }) - }; - }; - - const video = deepAccess(bid, 'mediaTypes.video'); - if (video) { - imp.video = getDefinedParams(video, VIDEO_PARAMS); - if (video.playerSize) { - imp.video = Object.assign( - imp.video, parseGPTSingleSizeArrayToRtbSize(video.playerSize[0]) || {} - ); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; - }; + imp: [], }; - postBody.imp.push(imp); - - const gdprConsent = bidderRequest && bidderRequest.gdprConsent; - const uspConsent = bidderRequest && bidderRequest.uspConsent; - - if (gdprConsent || uspConsent) { - postBody.regs = { ext: {} }; - - if (uspConsent) { - postBody.regs.ext.us_privacy = uspConsent; - }; - - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies !== 'undefined') { - postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; - }; - - if (typeof gdprConsent.consentString !== 'undefined') { - postBody.user = { - ext: { consent: gdprConsent.consentString } - }; - }; - }; - }; + postBody.imp.push(getImp(bid, id)); + setConsentStrings(postBody, bidderRequest); + setOrtb2Parameters(postBody, bidderRequest?.ortb2); + setEids(postBody, bid); const urlParameters = parseUrl(getWindowTop().location.href).search; const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; @@ -152,7 +122,7 @@ export const spec = { data: JSON.stringify(postBody), options: { contentType: 'text/plain', - withCredentials: true + withCredentials: true, }, bidId, @@ -174,6 +144,7 @@ export const spec = { 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}); @@ -190,8 +161,8 @@ export const spec = { netRevenue: true, ttl: TIME_TO_LIVE, meta: { - advertiserDomains: bid.adomain || [] - } + advertiserDomains: bid.adomain || [], + }, }; if (vastUrl || vastXml) { @@ -211,38 +182,29 @@ export const spec = { return bidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) return []; + const pixels = []; + const getSetPixelFunc = type => url => { pixels.push({type, url: replaceUsersyncMacros(url, gdprConsent, uspConsent, gppConsent, type)}) }; + const getSetPixelsFunc = type => response => { deepAccess(response, `body.ext.sync.${type}`, []).forEach(getSetPixelFunc(type)) }; + + const setPixel = (type, url) => { (getSetPixelFunc(type))(url) }; + const setPixelImages = getSetPixelsFunc('image'); + const setPixelIframes = getSetPixelsFunc('iframe'); if (isArray(responses)) { responses.forEach(response => { - if (syncOptions.pixelEnabled) { - deepAccess(response, 'body.ext.sync.image', []).forEach(imgUrl => { - pixels.push({ - type: 'image', - url: replaceUsersyncMacros(imgUrl, gdprConsent, uspConsent) - }); - }) - } - - if (syncOptions.iframeEnabled) { - deepAccess(response, 'body.ext.sync.iframe', []).forEach(iframeUrl => { - pixels.push({ - type: 'iframe', - url: replaceUsersyncMacros(iframeUrl, gdprConsent, uspConsent) - }); - }) - } + if (syncOptions.pixelEnabled) setPixelImages(response); + if (syncOptions.iframeEnabled) setPixelIframes(response); }) } if (!pixels.length) { - let syncUrl = SYNC_ENDPOINT; - if (gdprConsent && gdprConsent.gdprApplies) syncUrl += 'gdpr=1&gdpr_consent=' + gdprConsent.consentString + '&'; - if (uspConsent) syncUrl += 'us_privacy=' + uspConsent + '&'; - if (syncOptions.iframeEnabled) pixels.push({type: 'iframe', url: syncUrl + 'type=iframe'}); - if (syncOptions.pixelEnabled) pixels.push({type: 'image', url: syncUrl + 'type=image'}); + if (syncOptions.pixelEnabled) setPixel('image', SYNC_ENDPOINT); + if (syncOptions.iframeEnabled) setPixel('iframe', SYNC_ENDPOINT); } + return pixels; }, @@ -282,24 +244,94 @@ export const spec = { }, }; -function replaceUsersyncMacros(url, gdprConsent, uspConsent) { - const { consentString, gdprApplies } = gdprConsent || {}; +export function getImp(bid, id) { + const imp = { + id: bid.adUnitCode, + ext: { + prebid: { + storedrequest: { + id, + }, + }, + }, + }; + + const banner = deepAccess(bid, 'mediaTypes.banner'); + if (banner) { + imp.banner = { + format: (banner?.sizes || []).map(s => { return {w: s[0], h: s[1]} }), + }; + }; + + const video = deepAccess(bid, 'mediaTypes.video'); + if (video) { + imp.video = getDefinedParams(video, VIDEO_PARAMS); + if (video.playerSize) { + imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(video.playerSize) || {}); + } else if (video.w && video.h) { + imp.video.w = video.w; + imp.video.h = video.h; + }; + }; + + return imp; +}; + +export function setConsentStrings(postBody = {}, bidderRequest) { + const gdprConsent = bidderRequest?.gdprConsent; + const uspConsent = bidderRequest?.uspConsent; + let gppConsent = bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent; + if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) gppConsent = bidderRequest?.ortb2?.regs; - if (gdprApplies) { - const gdpr = Number(gdprApplies); - url = url.replace('{{.GDPR}}', gdpr); + if (gdprConsent || uspConsent || gppConsent) { + postBody.regs = { ext: {} }; - if (gdpr == 1 && consentString && consentString.length > 0) { - url = url.replace('{{.GDPRConsent}}', consentString); - } - } else { - url = url.replace('{{.GDPR}}', 0); - url = url.replace('{{.GDPRConsent}}', ''); - } + if (uspConsent) { + postBody.regs.ext.us_privacy = uspConsent; + }; + + if (gppConsent) { + postBody.regs.gpp = gppConsent?.gppString || gppConsent?.gpp; + postBody.regs.gpp_sid = bidderRequest.gppConsent?.applicableSections || gppConsent?.gpp_sid; + }; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + }; + + if (typeof gdprConsent.consentString !== 'undefined') { + postBody.user = { + ext: { consent: gdprConsent.consentString }, + }; + }; + }; + }; +}; - if (uspConsent) { - url = url.replace('{{.USPrivacy}}', uspConsent); +export function setOrtb2Parameters(postBody, ortb2 = {}) { + for (let parameter of ALLOWED_ORTB2_PARAMETERS) { + const value = deepAccess(ortb2, parameter); + if (value) deepSetValue(postBody, parameter, value); } +} + +export function setEids(postBody, bid) { + if (!isArray(bid.userIdAsEids) || !bid.userIdAsEids.length) return; + + deepSetValue(postBody, 'user.eids', bid.userIdAsEids); +} + +export function replaceUsersyncMacros(url, gdprConsent = {}, uspConsent = '', gppConsent = {}, type = '') { + const { consentString = '', gdprApplies = false } = gdprConsent; + const gdpr = Number(gdprApplies); + url = url + .replace('{{.GDPR}}', gdpr) + .replace('{{.GDPRConsent}}', consentString) + .replace('{{.USPrivacy}}', uspConsent) + .replace('{{.GPP}}', gppConsent.gppString || '') + .replace('{{.GPPSID}}', (gppConsent.applicableSections || []).join(',')) + .replace('{{.TYPE_PIXEL}}', type); return url; }; @@ -369,7 +401,7 @@ function getAd(bid) { } else if (bid.nurl) { adUrl = bid.nurl; }; - } + }; return {ad, adUrl, vastXml, vastUrl}; } @@ -377,10 +409,21 @@ function getAd(bid) { function getSiteObj() { const refInfo = (getRefererInfo && getRefererInfo()) || {}; + let language = navigator.language; + let content; + if (language) { + // get ISO-639-1-alpha-2 (2 character language) + language = language.split('-')[0]; + content = { + language, + }; + }; + return { page: refInfo.page, ref: refInfo.ref, - domain: refInfo.domain + domain: refInfo.domain, + content, }; } @@ -388,6 +431,19 @@ function getDeviceObj() { return { w: window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth || 0, h: window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight || 0, + ua: window.navigator.userAgent || undefined, + sua: getSua(), + }; +} + +function getSua() { + let {brands, mobile, platform} = (window?.navigator?.userAgentData || {}); + if (!(brands && platform)) return undefined; + + return { + brands, + mobile: Number(!!mobile), + platform: (platform && {brand: platform}) || undefined, }; } diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index 0dd4b334f6e..eab174d22dd 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,6 +1,5 @@ import { - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isNumber, diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index 3d8e9c348c8..c65544936fa 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -190,7 +190,7 @@ function interpretResponse(serverResponse) { demandSource: bid.ext.ssp, }, }; - if (allowAlternateBidderCodes) response.bidderCode = `n360-${bid.ext.ssp}`; + if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; if (bid.ext.mediaType === BANNER) { if (bid.adm) { diff --git a/modules/nobidAnalyticsAdapter.js b/modules/nobidAnalyticsAdapter.js new file mode 100644 index 00000000000..3a7912c37e1 --- /dev/null +++ b/modules/nobidAnalyticsAdapter.js @@ -0,0 +1,242 @@ +import {deepClone, logError, getParameterByName} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; + +const VERSION = '1.1.0'; +const MODULE_NAME = 'nobidAnalyticsAdapter'; +const ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS = 5 * 1000; +const RETENTION_SECONDS = 1 * 24 * 3600; +const TEST_ALLOCATION_PERCENTAGE = 5; // dont block 5% of the time; +window.nobidAnalyticsVersion = VERSION; +const analyticsType = 'endpoint'; +const url = 'localhost:8383/event'; +const GVLID = 816; +const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME, moduleType: MODULE_TYPE_ANALYTICS}); +const { + EVENTS: { + AUCTION_INIT, + BID_REQUESTED, + BID_TIMEOUT, + BID_RESPONSE, + BID_WON, + AUCTION_END, + AD_RENDER_SUCCEEDED + } +} = CONSTANTS; +function log (msg) { + // eslint-disable-next-line no-console + console.log(`%cNoBid Analytics ${VERSION}`, 'padding: 2px 8px 2px 8px; background-color:#f50057; color: white', msg); +} +function isJson (str) { + return str && str.startsWith('{') && str.endsWith('}'); +} +function isExpired (data, retentionSeconds) { + retentionSeconds = retentionSeconds || RETENTION_SECONDS; + if (data.ts + retentionSeconds * 1000 < Date.now()) return true; + return false; +} +function sendEvent (event, eventType) { + function resolveEndpoint() { + var ret = 'https://carbon-nv.servenobids.com/admin/status'; + var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; + if (!env) ret = 'https://carbon-nv.servenobids.com'; + else if (env == 'dev') ret = 'https://localhost:8383'; + return ret; + } + if (!nobidAnalytics.initOptions || !nobidAnalytics.initOptions.siteId || !event) return; + if (nobidAnalytics.isAnalyticsDisabled()) { + log('NoBid Analytics is Disabled'); + return; + } + try { + const endpoint = `${resolveEndpoint()}/event/${eventType}?pubid=${nobidAnalytics.initOptions.siteId}`; + ajax(endpoint, + function (response) { + try { + nobidAnalytics.processServerResponse(response); + } catch (e) { + logError(e); + } + }, + JSON.stringify(event), + { + contentType: 'application/json', + method: 'POST' + } + ); + } catch (err) { + log(`Sending event error ${err}`); + } +} +function cleanupObjectAttributes (obj, attributes) { + if (!obj) return; + if (Array.isArray(obj)) { + obj.forEach(item => { + Object.keys(item).forEach(attr => { if (!attributes.includes(attr)) delete item[attr] }); + }); + } else Object.keys(obj).forEach(attr => { if (!attributes.includes(attr)) delete obj[attr] }); +} +function sendBidWonEvent (event, eventType) { + const data = deepClone(event); + cleanupObjectAttributes(data, ['bidderCode', 'size', 'statusMessage', 'adId', 'requestId', 'mediaType', 'adUnitCode', 'cpm', 'timeToRespond']); + if (nobidAnalytics.topLocation) data.topLocation = nobidAnalytics.topLocation; + sendEvent(data, eventType); +} +function sendAuctionEndEvent (event, eventType) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } + const data = deepClone(event); + + cleanupObjectAttributes(data, ['timestamp', 'timeout', 'auctionId', 'bidderRequests', 'bidsReceived']); + if (data) cleanupObjectAttributes(data.bidderRequests, ['bidderCode', 'bidderRequestId', 'bids', 'refererInfo']); + if (data) cleanupObjectAttributes(data.bidsReceived, ['bidderCode', 'width', 'height', 'adUnitCode', 'statusMessage', 'requestId', 'mediaType', 'cpm']); + 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']); + sendEvent(data, eventType); +} +function auctionInit (event) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } +} +let nobidAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + auctionInit(args); + break; + case BID_REQUESTED: + break; + case BID_RESPONSE: + break; + case BID_WON: + sendBidWonEvent(args, eventType); + break; + case BID_TIMEOUT: + break; + case AUCTION_END: + sendAuctionEndEvent(args, eventType); + break; + case AD_RENDER_SUCCEEDED: + break; + default: + break; + } + } +}); + +nobidAnalytics = { + ...nobidAnalytics, + originEnableAnalytics: nobidAnalytics.enableAnalytics, // save the base class function + enableAnalytics: function (config) { // override enableAnalytics so we can get access to the config passed in from the page + if (!config.options.siteId) { + logError('NoBid Analytics - siteId parameter is not defined. Analytics won\'t work'); + return; + } + this.initOptions = config.options; + this.originEnableAnalytics(config); // call the base class function + }, + retentionSeconds: RETENTION_SECONDS, + isExpired (data) { + return isExpired(data, this.retentionSeconds); + }, + isAnalyticsDisabled () { + 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; + }, + processServerResponse (response) { + if (!isJson(response)) return; + const resp = JSON.parse(response); + storage.setDataInLocalStorage(this.ANALYTICS_DATA_NAME, JSON.stringify({ ...resp, ts: Date.now() })); + }, + ANALYTICS_DATA_NAME: 'analytics.nobid.io', + ANALYTICS_OPT_NAME: 'analytics.nobid.io.optData' +} + +adapterManager.registerAnalyticsAdapter({ + adapter: nobidAnalytics, + code: 'nobidAnalytics', + gvlid: GVLID +}); +nobidAnalytics.originalAdUnits = {}; +window.nobidCarbonizer = { + getStoredLocalData: function () { + const a = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + const b = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + const ret = {}; + if (a) ret[nobidAnalytics.ANALYTICS_DATA_NAME] = a; + if (b) ret[nobidAnalytics.ANALYTICS_OPT_NAME] = b + return ret; + }, + isActive: function () { + let stored = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return false; + return stored.carbonizer_active || false; + }, + carbonizeAdunits: function (adunits, skipTestGroup) { + function processBlockedBidders (blockedBidders) { + function sendOptimizerData() { + let optData = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + storage.removeDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + if (isJson(optData)) { + optData = JSON.parse(optData); + if (Object.getOwnPropertyNames(optData).length > 0) { + const event = { o_bidders: optData }; + if (nobidAnalytics.topLocation) event.topLocation = nobidAnalytics.topLocation; + sendEvent(event, 'optData'); + } + } + } + if (blockedBidders && blockedBidders.length > 0) { + let optData = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + optData = isJson(optData) ? JSON.parse(optData) : {}; + const bidders = blockedBidders.map(rec => rec.bidder); + if (bidders && bidders.length > 0) { + bidders.forEach(bidder => { + if (!optData[bidder]) optData[bidder] = 1; + else optData[bidder] += 1; + }); + storage.setDataInLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME, JSON.stringify(optData)); + if (window.nobidAnalyticsOptTimer) return; + window.nobidAnalyticsOptTimer = setInterval(sendOptimizerData, ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS); + } + } + } + function carbonizeAdunit (adunit) { + let stored = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return; + const carbonizerBidders = stored.bidders || []; + let originalAdUnit = null; + if (nobidAnalytics.originalAdUnits && nobidAnalytics.originalAdUnits[adunit.code]) originalAdUnit = nobidAnalytics.originalAdUnits[adunit.code]; + const allowedBidders = originalAdUnit.bids.filter(rec => carbonizerBidders.includes(rec.bidder)); + const blockedBidders = originalAdUnit.bids.filter(rec => !carbonizerBidders.includes(rec.bidder)); + processBlockedBidders(blockedBidders); + adunit.bids = allowedBidders; + } + for (const adunit of adunits) { + if (!nobidAnalytics.originalAdUnits[adunit.code]) nobidAnalytics.originalAdUnits[adunit.code] = JSON.parse(JSON.stringify(adunit)); + }; + if (this.isActive()) { + // 5% of the time do not block; + if (!skipTestGroup && Math.floor(Math.random() * 101) <= TEST_ALLOCATION_PERCENTAGE) return; + for (const adunit of adunits) { + carbonizeAdunit(adunit); + }; + } + } +}; +export default nobidAnalytics; diff --git a/modules/nobidAnalyticsAdapter.md b/modules/nobidAnalyticsAdapter.md new file mode 100644 index 00000000000..92b9bdbb3cb --- /dev/null +++ b/modules/nobidAnalyticsAdapter.md @@ -0,0 +1,38 @@ +# Overview +Module Name: NoBid Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [nobid.io](https://nobid.io) + + +# NoBid Analytics Registration + +The NoBid Analytics Adapter is free to use during our Beta period, but requires a simple registration with NoBid. Please visit [www.nobid.io](https://www.nobid.io/contact-1/) to sign up and request your NoBid Site ID to get started. If you're already using the NoBid Prebid Adapter, you may use your existing Site ID with the NoBid Analytics Adapter. + +The NoBid privacy policy is at [nobid.io/privacy-policy](https://www.nobid.io/privacy-policy/). + +## NoBid Analytics Configuration + +First, make sure to add the NoBid Analytics submodule to your Prebid.js package with: + +``` +gulp build --modules=...,nobidAnalyticsAdapter... +``` + +The following configuration parameters are available: + +```javascript +pbjs.enableAnalytics({ + provider: 'nobidAnalytics', + options: { + siteId: 123 // change to the Site ID you received from NoBid + } +}); +``` + +{: .table .table-bordered .table-striped } +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| provider | Required | String | The name of this module: `nobidAnalytics` | `nobidAnalytics` | +| options.siteId | Required | Number | This is the NoBid Site ID Number obtained from registering with NoBid. | `1234` | diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 68010b32b37..77de3c6d97b 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -367,21 +367,21 @@ export const spec = { ], supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { log('isBidRequestValid', bid); return !!bid.params.siteId; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { function resolveEndpoint() { var ret = 'https://ads.servenobid.com/'; @@ -421,11 +421,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { log('interpretResponse -> serverResponse', serverResponse); log('interpretResponse -> bidRequest', bidRequest); @@ -433,12 +433,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, usPrivacy, gppConsent) { if (syncOptions.iframeEnabled) { let params = ''; @@ -483,9 +483,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if bidder timed out after an auction - * @param {data} Containing timeout specific data - */ + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ onTimeout: function(data) { window.nobid.timeoutTotal++; log('Timeout total: ' + window.nobid.timeoutTotal, data); diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index 7eced81d35e..d0cc94b513b 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -17,9 +17,9 @@ const MODULE_NAME = 'novatiq'; export const novatiqIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** * used to specify vendor id @@ -28,10 +28,10 @@ export const novatiqIdSubmodule = { gvlid: 1119, /** - * decode the stored id value for passing to bid requests - * @function - * @returns {novatiq: {snowflake: string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @returns {novatiq: {snowflake: string}} + */ decode(novatiqId, config) { let responseObj = { novatiq: { @@ -52,11 +52,11 @@ export const novatiqIdSubmodule = { }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} config - * @returns {id: string} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @returns {id: string} + */ getId(config) { const configParams = config.params || {}; const urlParams = this.getUrlParams(configParams); diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 4fd9b711b42..9937391f6e7 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -1,9 +1,10 @@ 'use strict'; import {BANNER} from '../src/mediaTypes.js'; -import {getAdUnitSizes, getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; +import {getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ajax} from '../src/ajax.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ogury'; const GVLID = 31; @@ -11,7 +12,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.5.0'; +const ADAPTER_VERSION = '1.6.0'; function getClientWidth() { const documentElementClientWidth = window.top.document.documentElement.clientWidth @@ -121,6 +122,13 @@ function buildRequests(validBidRequests, bidderRequest) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; const floor = getFloor(bidRequest); + if (bidRequest.userId) { + openRtbBidRequestBanner.user.ext.uids = bidRequest.userId + } + if (bidRequest.userIdAsEids) { + openRtbBidRequestBanner.user.ext.eids = bidRequest.userIdAsEids + } + openRtbBidRequestBanner.imp.push({ id: bidRequest.bidId, tagid: bidRequest.params.adUnitId, diff --git a/modules/oneKeyIdSystem.js b/modules/oneKeyIdSystem.js index 699a7a6ab95..7aa9e7b48ac 100644 --- a/modules/oneKeyIdSystem.js +++ b/modules/oneKeyIdSystem.js @@ -47,27 +47,27 @@ const getIdsAndPreferences = (callback) => { /** @type {Submodule} */ export const oneKeyIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: 'oneKeyData', /** - * decode the stored data value for passing to bid requests - * @function decode - * @param {(Object|string)} value - * @returns {(Object|undefined)} - */ + * decode the stored data value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ decode(data) { return { oneKeyData: data }; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @param {(Object|undefined)} cacheIdObj - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ getId(config) { return { callback: getIdsAndPreferences diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 801bb747e34..ee6d2980385 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -87,6 +87,7 @@ function buildRequests(validBidRequests, bidderRequest) { const connection = navigator.connection || navigator.webkitConnection; payload.networkConnectionType = (connection && connection.type) ? connection.type : null; payload.networkEffectiveConnectionType = (connection && connection.effectiveType) ? connection.effectiveType : null; + payload.fledgeEnabled = Boolean(bidderRequest && bidderRequest.fledgeEnabled) return { method: 'POST', url: ENDPOINT, @@ -101,10 +102,10 @@ function interpretResponse(serverResponse, bidderRequest) { if (!body || (body.nobid && body.nobid === true)) { return bids; } - if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { + if (!body.fledgeAuctionConfigs && (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0)) { return bids; } - body.bids.forEach(bid => { + Array.isArray(body.bids) && body.bids.forEach(bid => { const responseBid = { requestId: bid.requestId, cpm: bid.cpm, @@ -141,7 +142,16 @@ function interpretResponse(serverResponse, bidderRequest) { } bids.push(responseBid); }); - return bids; + + if (body.fledgeAuctionConfigs && Array.isArray(body.fledgeAuctionConfigs)) { + const fledgeAuctionConfigs = body.fledgeAuctionConfigs + return { + bids, + fledgeAuctionConfigs, + } + } else { + return bids; + } } function createRenderer(bid, rendererOptions = {}) { @@ -267,9 +277,8 @@ function setGeneralInfo(bidRequest) { this['adUnitCode'] = bidRequest.adUnitCode; this['bidId'] = bidRequest.bidId; this['bidderRequestId'] = bidRequest.bidderRequestId; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - this['auctionId'] = bidRequest.auctionId; - this['transactionId'] = bidRequest.ortb2Imp?.ext?.tid; + this['auctionId'] = deepAccess(bidRequest, 'ortb2.source.tid'); + this['transactionId'] = deepAccess(bidRequest, 'ortb2Imp.ext.tid'); this['gpid'] = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); this['pubId'] = params.pubId; this['ext'] = params.ext; diff --git a/modules/onomagicBidAdapter.js b/modules/onomagicBidAdapter.js index edab625e541..78f00153a8b 100644 --- a/modules/onomagicBidAdapter.js +++ b/modules/onomagicBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - createTrackPixelHtml, - getBidIdParameter, + createTrackPixelHtml, getBidIdParameter, getUniqueIdentifierStr, getWindowSelf, getWindowTop, diff --git a/modules/open8BidAdapter.js b/modules/open8BidAdapter.js index 5fa1dd0a143..49523926c0e 100644 --- a/modules/open8BidAdapter.js +++ b/modules/open8BidAdapter.js @@ -1,8 +1,9 @@ import { Renderer } from '../src/Renderer.js'; import {ajax} from '../src/ajax.js'; -import { createTrackPixelHtml, getBidIdParameter, logError, logWarn, tryAppendQueryString } from '../src/utils.js'; +import {createTrackPixelHtml, getBidIdParameter, logError, logWarn} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'open8'; const URL = 'https://as.vt.open8.com/v1/control/prebid'; diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 296bfc682f1..547447039da 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,8 +1,9 @@ -import {convertTypes, deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; +import {deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const ENDPOINT = 'https://ghb.spotim.market/v2/auction'; const BIDDER_CODE = 'openweb'; diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 03423a028b4..0f8bee213f7 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -4,6 +4,7 @@ import * as utils from '../src/utils.js'; import {mergeDeep} from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const bidderConfig = 'hb_pb_ortb'; const bidderVersion = '2.0'; @@ -12,6 +13,7 @@ export const SYNC_URL = 'https://u.openx.net/w/1.0/pd'; export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba'; export const spec = { code: 'openx', + gvlid: 69, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, buildRequests, @@ -48,7 +50,8 @@ const converter = ortbConverter({ mergeDeep(req, { at: 1, ext: { - bc: `${bidderConfig}_${bidderVersion}` + bc: `${bidderConfig}_${bidderVersion}`, + pv: '$prebid.version$' } }) const bid = context.bidRequests[0]; @@ -149,7 +152,7 @@ const converter = ortbConverter({ }); function transformBidParams(params, isOpenRtb) { - return utils.convertTypes({ + return convertTypes({ 'unit': 'string', 'customFloor': 'number' }, params); diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index b45c0452319..131ba0bc1f2 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -70,7 +70,7 @@ const NATIVE_DEFAULTS = { export const spec = { code: BIDDER_CODE, - + gvlid: 1135, // short code aliases: ['opera'], @@ -232,13 +232,6 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { test: config.getConfig('debug') ? 1 : 0, imp: createImp(bidRequest), device: getDevice(), - site: { - id: String(deepAccess(bidRequest, 'params.publisherId')), - // TODO: does the fallback make sense here? - domain: bidderRequest?.refererInfo?.domain || window.location.host, - page: bidderRequest?.refererInfo?.page, - ref: bidderRequest?.refererInfo?.ref || '', - }, at: 1, bcat: getBcat(bidRequest), cur: [DEFAULT_CURRENCY], @@ -250,6 +243,7 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { buyeruid: getUserId(bidRequest) } } + fulfillInventoryInfo(payload, bidRequest, bidderRequest); const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { @@ -764,6 +758,38 @@ function getDevice() { return device; } +/** + * Fulfill inventory info + * + * @param payload + * @param bidRequest + * @param bidderRequest + */ +function fulfillInventoryInfo(payload, bidRequest, bidderRequest) { + let info = deepAccess(bidRequest, 'params.site'); + // 1.If the inventory info for site specified, use the site object provided in params. + let key = 'site'; + if (!isPlainObject(info)) { + info = deepAccess(bidRequest, 'params.app'); + if (isPlainObject(info)) { + // 2.If the inventory info for app specified, use the app object provided in params. + key = 'app'; + } else { + // 3.Otherwise, we use site by default. + info = {}; + } + } + // Fulfill key parameters. + info.id = String(deepAccess(bidRequest, 'params.publisherId')); + info.domain = info.domain || bidderRequest?.refererInfo?.domain || window.location.host; + if (key === 'site') { + info.ref = info.ref || bidderRequest?.refererInfo?.ref || ''; + info.page = info.page || bidderRequest?.refererInfo?.page; + } + + payload[key] = info; +} + /** * Get browser language * diff --git a/modules/operaadsBidAdapter.md b/modules/operaadsBidAdapter.md index 6c5a4646dd0..6f13eebd7d5 100644 --- a/modules/operaadsBidAdapter.md +++ b/modules/operaadsBidAdapter.md @@ -14,41 +14,43 @@ Module that connects to OperaAds's demand sources ## Bid Parameters -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` -| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` -| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` -| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------------------|-----------------------------------------|-------------------------------------------------| +| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` | +| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` | +| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` | +| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` | +| `site` | optional | Object | The site information. | `{"name": "my_site", "domain": "www.test.com"}` | +| `app` | optional | Object | The app information. | `{"name": "my_app", "ver": "1.1.0"}` | ### Bid Video Parameters Set these parameters to `bid.mediaTypes.video`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `context` | optional | String | `instream` or `outstream`. | `instream` -| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` -| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` -| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` -| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` -| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` -| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` -| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` -| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` +| Name | Scope | Type | Description | Example | +|------------------|----------|------------------------|------------------------------------------------------------------------------------------|----------------------------| +| `context` | optional | String | `instream` or `outstream`. | `instream` | +| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` | +| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` | +| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` | +| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` | +| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` | +| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` | +| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` | +| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` | ### Bid Native Parameters Set these parameters to `bid.nativeParams` or `bid.mediaTypes.native`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` -| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` -| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` -| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` -| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` -| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` | +| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` | +| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` | +| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` | +| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` | +| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` | ## Example @@ -127,7 +129,9 @@ var adUnits = [{ params: { placementId: 's5340077725248', endpointId: 'ep3425464070464', - publisherId: 'pub3054952966336' + publisherId: 'pub3054952966336', + // You might want to specify some application information here if the bid requests are from an application instead of a browser. + app: { 'name': 'my_app', 'bundle': 'test_bundle', 'store_url': 'www.some-store.com', 'ver': '1.1.0' } } }] }]; diff --git a/modules/operaadsIdSystem.js b/modules/operaadsIdSystem.js index 09dd8512a2b..c16d5a9c009 100644 --- a/modules/operaadsIdSystem.js +++ b/modules/operaadsIdSystem.js @@ -50,33 +50,33 @@ function asyncRequest(url, cb) { export const operaIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * @type {string} - */ + * @type {string} + */ version, /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} id - * @returns {{'operaId': string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} id + * @returns {{'operaId': string}} + */ decode: (id) => id != null && id.length > 0 ? { [ID_KEY]: id } : undefined, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ getId(config, consentData) { logMessage(`${MODULE_NAME}: start synchronizing opera uid`); const params = (config && config.params) || {}; diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js index 9f84ff034ea..152876b8d5d 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -1,6 +1,7 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { deepAccess, parseSizesInput, getAdUnitSizes } from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {deepAccess, parseSizesInput} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'optidigital'; const GVL_ID = 915; @@ -14,11 +15,11 @@ export const spec = { gvlid: GVL_ID, supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { let isValid = false; if (typeof bid.params !== 'undefined' && bid.params.placementId && bid.params.publisherId) { @@ -28,11 +29,11 @@ export const spec = { return isValid; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { if (!validBidRequests || validBidRequests.length === 0 || !bidderRequest || !bidderRequest.bids) { return []; @@ -90,6 +91,12 @@ export const spec = { payload.uspConsent = bidderRequest.uspConsent; } + if (_getEids(validBidRequests[0])) { + payload.user = { + eids: _getEids(validBidRequests[0]) + } + } + const payloadObject = JSON.stringify(payload); return { method: 'POST', @@ -98,11 +105,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { const bidResponses = []; serverResponse = serverResponse.body; @@ -131,12 +138,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncurl = ''; if (!isSynced) { @@ -223,6 +230,12 @@ function _getFloor (bid, sizes, currency) { return floor !== null ? floor : bid.params.floor; } +function _getEids(bidRequest) { + if (deepAccess(bidRequest, 'userIdAsEids')) { + return bidRequest.userIdAsEids; + } +} + export function resetSync() { isSynced = false; } diff --git a/modules/optidigitalBidAdapter.md b/modules/optidigitalBidAdapter.md index 466dfb3bef2..327e7a27c75 100755 --- a/modules/optidigitalBidAdapter.md +++ b/modules/optidigitalBidAdapter.md @@ -37,10 +37,13 @@ Bidder Adapter for Prebid.js. ``` pbjs.setConfig({ - userSync: { - iframeEnabled: true, - syncEnabled: true, - syncDelay: 3000 - } +  userSync: { +    filterSettings: { +      iframe: { +        bidders: '*', // '*' represents all bidders +        filter: 'include' +      } +    } +  } }); ``` diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index dfe8f1bfcf2..074d8d39bbf 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -16,6 +16,7 @@ * @property {string} clientID * @property {string} optimeraKeyName * @property {string} device + * @property {string} apiVersion */ import { logInfo, logError } from '../src/utils.js'; @@ -29,7 +30,8 @@ let _moduleParams = {}; * Default Optimera Key Name * This can default to hb_deal_optimera for publishers * who used the previous Optimera Bidder Adapter. - * @type {string} */ + * @type {string} + */ export let optimeraKeyName = 'hb_deal_optimera'; /** @@ -38,7 +40,10 @@ export let optimeraKeyName = 'hb_deal_optimera'; * the targeting values. * @type {string} */ -export const scoresBaseURL = 'https://dyv1bugovvq1g.cloudfront.net/'; +export const scoresBaseURL = { + v0: 'https://dyv1bugovvq1g.cloudfront.net/', + v1: 'https://v1.oapi26b.com/', +}; /** * Optimera Score File URL. @@ -58,6 +63,12 @@ export let clientID; */ export let device = 'default'; +/** + * Optional apiVersion parameter. + * @type {string} + */ +export let apiVersion = 'v0'; + /** * Targeting object for all ad positions. * @type {string} @@ -127,6 +138,7 @@ export function onAuctionInit(auctionDetails, config, userConsent) { /** * Initialize the Module. + * moduleConfig.params.apiVersion can be either v0 or v1. */ export function init(moduleConfig) { _moduleParams = moduleConfig.params; @@ -138,6 +150,9 @@ export function init(moduleConfig) { if (_moduleParams.device) { device = _moduleParams.device; } + if (_moduleParams.apiVersion) { + apiVersion = (_moduleParams.apiVersion.includes('v1', 'v0')) ? _moduleParams.apiVersion : 'v0'; + } setScoresURL(); scoreFileRequest(); return true; @@ -162,7 +177,15 @@ export function init(moduleConfig) { export function setScoresURL() { const optimeraHost = window.location.host; const optimeraPathName = window.location.pathname; - const newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; + const baseUrl = scoresBaseURL[apiVersion] ? scoresBaseURL[apiVersion] : scoresBaseURL.v0; + let newScoresURL; + + if (apiVersion === 'v1') { + newScoresURL = `${baseUrl}api/products/scores?c=${clientID}&h=${optimeraHost}&p=${optimeraPathName}&s=${device}`; + } else { + newScoresURL = `${baseUrl}${clientID}/${optimeraHost}${optimeraPathName}.js`; + } + if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; fetchScoreFile = true; diff --git a/modules/optimeraRtdProvider.md b/modules/optimeraRtdProvider.md index 610dec537e0..8b66deb5ad5 100644 --- a/modules/optimeraRtdProvider.md +++ b/modules/optimeraRtdProvider.md @@ -1,6 +1,6 @@ # Overview ``` -Module Name: Optimera Real Time Date Module +Module Name: Optimera Real Time Data Module Module Type: RTD Module Maintainer: mcallari@optimera.nyc ``` @@ -26,7 +26,8 @@ Configuration example for using RTD module with `optimera` provider params: { clientID: '9999', optimeraKeyName: 'optimera', - device: 'de' + device: 'de', + apiVersion: 'v0', } } ] @@ -42,3 +43,4 @@ Contact Optimera to get assistance with the params. | clientID | string | required | Optimera Client ID | | optimeraKeyName | string | optional | GAM key name for Optimera. If migrating from the Optimera bidder adapter this will default to hb_deal_optimera and can be ommitted from the configuration. | | device | string | optional | Device type code for mobile, tablet, or desktop. Either mo, tb, de | +| apiVersion | string | optional | Optimera API Versions. Either v0, or v1. ** Note: v1 wll need to be enabled specifically for your account, otherwise use v0. \ No newline at end of file diff --git a/modules/optimonAnalyticsAdapter.js b/modules/optimonAnalyticsAdapter.js index 82bc18f605d..68baf007563 100644 --- a/modules/optimonAnalyticsAdapter.js +++ b/modules/optimonAnalyticsAdapter.js @@ -1,12 +1,12 @@ /** -* -********************************************************* -* -* Optimon.io Prebid Analytics Adapter -* -********************************************************* -* -*/ + * + ********************************************************* + * + * Optimon.io Prebid Analytics Adapter + * + ********************************************************* + * + */ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index f135ebb2bd1..efc2effdd62 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,11 +1,11 @@ import { isFn, isPlainObject } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { getGlobal } from '../src/prebidGlobal.js'; -const storageManager = getStorageManager({bidderCode: 'orbidder'}); +const storageManager = getStorageManager({ bidderCode: 'orbidder' }); /** * Determines whether or not the given bid response is valid. @@ -69,7 +69,7 @@ export const spec = { return !!(bid.sizes && bid.bidId && bid.params && (bid.params.accountId && (typeof bid.params.accountId === 'string')) && (bid.params.placementId && (typeof bid.params.placementId === 'string')) && - ((typeof bid.params.profile === 'undefined') || (typeof bid.params.profile === 'object'))); + ((typeof bid.params.keyValues === 'undefined') || (typeof bid.params.keyValues === 'object'))); }, /** @@ -99,15 +99,7 @@ export const spec = { data: { v: getGlobal().version, pageUrl: referer, - bidId: bidRequest.bidId, - auctionId: bidRequest.auctionId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - transactionId: bidRequest.ortb2Imp?.ext?.tid, - adUnitCode: bidRequest.adUnitCode, - bidRequestCount: bidRequest.bidRequestCount, - params: bidRequest.params, - sizes: bidRequest.sizes, - mediaTypes: bidRequest.mediaTypes + ...bidRequest // get all data provided by bid request } }; diff --git a/modules/orbitsoftBidAdapter.js b/modules/orbitsoftBidAdapter.js index 4c3f2e38c58..f55c7ff9917 100644 --- a/modules/orbitsoftBidAdapter.js +++ b/modules/orbitsoftBidAdapter.js @@ -1,5 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'orbitsoft'; let styleParamsMap = { @@ -45,10 +46,10 @@ export const spec = { for (let i = 0; i < validBidRequests.length; i++) { bidRequest = validBidRequests[i]; let bidRequestParams = bidRequest.params; - let placementId = utils.getBidIdParameter('placementId', bidRequestParams); - let requestUrl = utils.getBidIdParameter('requestUrl', bidRequestParams); - let referrer = utils.getBidIdParameter('ref', bidRequestParams); - let location = utils.getBidIdParameter('loc', bidRequestParams); + let placementId = getBidIdParameter('placementId', bidRequestParams); + let requestUrl = getBidIdParameter('requestUrl', bidRequestParams); + let referrer = getBidIdParameter('ref', bidRequestParams); + let location = getBidIdParameter('loc', bidRequestParams); // Append location & referrer if (location === '') { location = utils.getWindowLocation(); @@ -58,7 +59,7 @@ export const spec = { } // Styles params - let stylesParams = utils.getBidIdParameter('style', bidRequestParams); + let stylesParams = getBidIdParameter('style', bidRequestParams); let stylesParamsArray = {}; for (let currentValue in stylesParams) { if (stylesParams.hasOwnProperty(currentValue)) { @@ -74,7 +75,7 @@ export const spec = { } } // Custom params - let customParams = utils.getBidIdParameter('customParams', bidRequestParams); + let customParams = getBidIdParameter('customParams', bidRequestParams); let customParamsArray = {}; for (let customField in customParams) { if (customParams.hasOwnProperty(customField)) { diff --git a/modules/otmBidAdapter.js b/modules/otmBidAdapter.js index 6125cee6593..7d4049e3054 100644 --- a/modules/otmBidAdapter.js +++ b/modules/otmBidAdapter.js @@ -2,14 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { logInfo, logError, - getBidIdParameter, _each, getValue, isFn, isPlainObject, isArray, isStr, - isNumber, + isNumber, getBidIdParameter, } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 0637d680912..6015ff37e08 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; import {OUTSTREAM} from '../src/video.js'; import {_map, deepAccess, deepSetValue, isArray, logWarn, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; @@ -23,6 +24,9 @@ const NATIVE_PARAMS = { cta: { id: 1, type: 12, name: 'data' } }; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const OB_USER_TOKEN_KEY = 'OB-USER-TOKEN'; + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -130,6 +134,11 @@ export const spec = { request.test = 1; } + const obUserToken = storage.getDataFromLocalStorage(OB_USER_TOKEN_KEY) + if (obUserToken) { + deepSetValue(request, 'user.ext.obusertoken', obUserToken) + } + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) @@ -140,6 +149,13 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) } + if (bidderRequest.gppConsent) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.gppConsent.gppString) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.gppConsent.applicableSections) + } else if (deepAccess(bidderRequest, 'ortb2.regs.gpp')) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.ortb2.regs.gpp) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.ortb2.regs.gpp_sid) + } if (eids) { deepSetValue(request, 'user.ext.eids', eids); @@ -203,7 +219,7 @@ export const spec = { } }).filter(Boolean); }, - getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { const syncs = []; let syncUrl = config.getConfig('outbrain.usersyncUrl'); @@ -216,6 +232,10 @@ export const spec = { if (uspConsent) { query.push('us_privacy=' + encodeURIComponent(uspConsent)); } + if (gppConsent) { + query.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + query.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','))); + } syncs.push({ type: 'image', diff --git a/modules/oxxionAnalyticsAdapter.js b/modules/oxxionAnalyticsAdapter.js index cc69443d8bf..25732d440ff 100644 --- a/modules/oxxionAnalyticsAdapter.js +++ b/modules/oxxionAnalyticsAdapter.js @@ -23,7 +23,7 @@ let auctionEnd = {} let initOptions = {} let mode = {}; let endpoint = 'https://default' -let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId']; +let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId', 'ova']; function getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; @@ -183,6 +183,15 @@ function handleBidWon(args) { } }); } + if (auction['auctionId'] == args['auctionId'] && typeof auction['bidderRequests'] == 'object') { + auction['bidderRequests'].forEach((req) => { + req.bids.forEach((bid) => { + if (bid['bidId'] == args['requestId'] && bid['transactionId'] == args['transactionId']) { + args['ova'] = bid['ova']; + } + }); + }); + } }); } args['cpmIncrement'] = increment; @@ -232,7 +241,8 @@ let oxxionAnalytics = Object.assign(adapter({url, analyticsType}), { addTimeout(args); break; } - }}); + } +}); // save the base class function oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; diff --git a/modules/oxxionRtdProvider.js b/modules/oxxionRtdProvider.js index c6f8b9a902b..f968fc10e4b 100644 --- a/modules/oxxionRtdProvider.js +++ b/modules/oxxionRtdProvider.js @@ -1,12 +1,10 @@ import { submodule } from '../src/hook.js' -import { deepAccess, logInfo, logError } from '../src/utils.js' +import { logInfo, logError } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import adapterManager from '../src/adapterManager.js'; -const oxxionRtdSearchFor = [ 'adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'userId', 'labelAny', 'adId' ]; const LOG_PREFIX = 'oxxionRtdProvider submodule: '; -const allAdUnits = []; const bidderAliasRegistry = adapterManager.aliasRegistry || {}; /** @type {RtdSubmodule} */ @@ -14,19 +12,18 @@ export const oxxionSubmodule = { name: 'oxxionRtd', init: init, getBidRequestData: getAdUnits, - onBidResponseEvent: insertVideoTracking, getRequestsList: getRequestsList, getFilteredAdUnitsOnBidRates: getFilteredAdUnitsOnBidRates, }; function init(config, userConsent) { if (!config.params || !config.params.domain) { return false } - if (config.params.contexts && Array.isArray(config.params.contexts) && config.params.contexts.length > 0) { return true; } if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { return true } return false; } function getAdUnits(reqBidsConfigObj, callback, config, userConsent) { + const moduleStarted = new Date(); logInfo(LOG_PREFIX + 'started with ', config); if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { let filteredBids; @@ -51,83 +48,13 @@ function getAdUnits(reqBidsConfigObj, callback, config, userConsent) { }); } if (typeof callback == 'function') { callback(); } - }).catch(error => logError(LOG_PREFIX, 'bidInterestError', error)); - } - if (config.params.contexts && Array.isArray(config.params.contexts) && config.params.contexts.length > 0) { - const reqAdUnits = reqBidsConfigObj.adUnits; - if (Array.isArray(reqAdUnits)) { - reqAdUnits.forEach(adunit => { - if (config.params.contexts.includes(deepAccess(adunit, 'mediaTypes.video.context'))) { - allAdUnits.push(adunit); - } - }); - } - if (!(typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') && typeof callback == 'function') { - callback(); - } - } -} - -function insertVideoTracking(bidResponse, config, userConsent) { - // this should only be do for video bids - if (bidResponse.mediaType === 'video') { - let maxCpm = 0; - const trackingUrl = getImpUrl(config, bidResponse, maxCpm); - if (!trackingUrl) { - return; - } - // Vast Impression URL - if (bidResponse.vastUrl) { - bidResponse.vastImpUrl = bidResponse.vastImpUrl - ? trackingUrl + '&url=' + encodeURI(bidResponse.vastImpUrl) - : trackingUrl; - logInfo(LOG_PREFIX + 'insert into vastImpUrl for adId ' + bidResponse.adId); - } - // Vast XML document - if (bidResponse.vastXml !== undefined) { - const doc = new DOMParser().parseFromString(bidResponse.vastXml, 'text/xml'); - const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine'); - let hasAltered = false; - if (wrappers.length) { - wrappers.forEach(wrapper => { - const impression = doc.createElement('Impression'); - impression.appendChild(doc.createCDATASection(trackingUrl)); - wrapper.appendChild(impression) - }); - bidResponse.vastXml = new XMLSerializer().serializeToString(doc); - hasAltered = true; + const timeToRun = new Date() - moduleStarted; + logInfo(LOG_PREFIX + ' time to run: ' + timeToRun); + if (getRandomNumber(50) == 1) { + ajax('https://' + config.params.domain + '.oxxion.io/ova/time', null, JSON.stringify({'duration': timeToRun, 'auctionId': reqBidsConfigObj.auctionId}), {method: 'POST', withCredentials: true}); } - if (hasAltered) { - logInfo(LOG_PREFIX + 'insert into vastXml for adId ' + bidResponse.adId); - } - } - } -} - -function getImpUrl(config, data, maxCpm) { - const adUnitCode = data.adUnitCode; - const adUnits = allAdUnits.find(adunit => adunit.code === adUnitCode && - 'mediaTypes' in adunit && - 'video' in adunit.mediaTypes && - typeof adunit.mediaTypes.video.context === 'string'); - const context = adUnits !== undefined - ? adUnits.mediaTypes.video.context - : 'unknown'; - if (!config.params.contexts.includes(context)) { - return false; + }).catch(error => logError(LOG_PREFIX, 'bidInterestError', error)); } - let trackingImpUrl = 'https://' + config.params.domain + '.oxxion.io/analytics/vast_imp?'; - trackingImpUrl += oxxionRtdSearchFor.reduce((acc, param) => { - switch (typeof data[param]) { - case 'string': - case 'number': - acc += param + '=' + data[param] + '&' - break; - } - return acc; - }, ''); - const cpmIncrement = 0.0; - return trackingImpUrl + 'cpmIncrement=' + cpmIncrement + '&context=' + context; } function getPromisifiedAjax (url, data = {}, options = {}) { @@ -146,22 +73,27 @@ function getPromisifiedAjax (url, data = {}, options = {}) { function getFilteredAdUnitsOnBidRates (bidsRateInterests, adUnits, params, useSampling) { const { threshold, samplingRate } = params; + const sampling = getRandomNumber(100) < samplingRate && useSampling; const filteredBids = []; // Separate bidsRateInterests in two groups against threshold & samplingRate - const { interestingBidsRates, uninterestingBidsRates } = bidsRateInterests.reduce((acc, interestingBid) => { + const { interestingBidsRates, uninterestingBidsRates, sampledBidsRates } = bidsRateInterests.reduce((acc, interestingBid) => { const isBidRateUpper = typeof threshold == 'number' ? interestingBid.rate === true || interestingBid.rate > threshold : interestingBid.suggestion; - const isBidInteresting = isBidRateUpper || (getRandomNumber(100) < samplingRate && useSampling); + const isBidInteresting = isBidRateUpper || sampling; const key = isBidInteresting ? 'interestingBidsRates' : 'uninterestingBidsRates'; acc[key].push(interestingBid); + if (!isBidRateUpper && sampling) { + acc['sampledBidsRates'].push(interestingBid); + } return acc; }, { interestingBidsRates: [], - uninterestingBidsRates: [] // Do something with later + uninterestingBidsRates: [], // Do something with later + sampledBidsRates: [] }); logInfo(LOG_PREFIX, 'getFilteredAdUnitsOnBidRates()', interestingBidsRates, uninterestingBidsRates); // Filter bids and adUnits against interesting bids rates const newAdUnits = adUnits.filter(({ bids = [] }, adUnitIndex) => { - adUnits[adUnitIndex].bids = bids.filter(bid => { + adUnits[adUnitIndex].bids = bids.filter((bid, bidIndex) => { if (!params.bidders || params.bidders.includes(bid.bidder)) { const index = interestingBidsRates.findIndex(({ id }) => id === bid._id); if (index == -1) { @@ -173,10 +105,19 @@ function getFilteredAdUnitsOnBidRates (bidsRateInterests, adUnits, params, useSa delete tmpBid.floorData; } filteredBids.push(tmpBid); + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'filtered'; + } else { + if (sampledBidsRates.findIndex(({ id }) => id === bid._id) == -1) { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'cleared'; + } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'sampled'; + logInfo(LOG_PREFIX + ' sampled ! '); + } } delete bid._id; return index !== -1; } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'protected'; return true; } }); diff --git a/modules/oxxionRtdProvider.md b/modules/oxxionRtdProvider.md index 14b4abec5c2..bfdbfae1fa9 100644 --- a/modules/oxxionRtdProvider.md +++ b/modules/oxxionRtdProvider.md @@ -7,7 +7,7 @@ Maintainer: tech@oxxion.io # Oxxion Real-Time-Data submodule Oxxion helps you to understand how your prebid stack performs. -This Rtd module is to use in order to improve video events tracking and/or to filter bidder requested. +This Rtd module purpose is to filter bidders requested. # Integration @@ -30,7 +30,6 @@ pbjs.setConfig( waitForIt: true, params: { domain: "test.endpoint", - contexts: ["instream"], threshold: false, samplingRate: 10, } @@ -47,12 +46,6 @@ pbjs.setConfig( |:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------| | domain | String | This string identifies yourself in Oxxion's systems and is provided to you by your Oxxion representative. | -# setConfig Parameters for Video Tracking - -| Name | Type | Description | -|:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------| -| contexts | Array | Array defining which video contexts to add tracking events into. Values can be instream and/or outstream. | - # setConfig Parameters for bidder filtering | Name | Type | Description | diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 970c7d49fb9..0d921f57cda 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -22,10 +22,10 @@ const AUCTIONURI = '/openrtb2/auction'; const OZONECOOKIESYNC = '/static/load-cookie.html'; const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js'; const ORIGIN_DEV = 'https://test.ozpr.net'; -const OZONEVERSION = '2.9.0'; +const OZONEVERSION = '2.9.1'; export const spec = { gvlid: 524, - aliases: [{code: 'lmc', gvlid: 524}], + aliases: [{code: 'lmc', gvlid: 524}, {code: 'venatus', gvlid: 524}], version: OZONEVERSION, code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], @@ -78,6 +78,9 @@ export const spec = { if (bidderConfig.hasOwnProperty('batchRequests')) { this.propertyBag.whitelabel.batchRequests = bidderConfig.batchRequests; } + if (arr.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = true; + } try { if (arr.hasOwnProperty('auction') && arr.auction === 'dev') { logInfo('GET: auction=dev'); @@ -100,6 +103,7 @@ export const spec = { return this.propertyBag.whitelabel.rendererUrl; }, isBatchRequests() { + logInfo('isBatchRequests going to return ', this.propertyBag.whitelabel.batchRequests); return this.propertyBag.whitelabel.batchRequests; }, isBidRequestValid(bid) { diff --git a/modules/pairIdSystem.js b/modules/pairIdSystem.js index 489b97d02e3..1009846e06a 100644 --- a/modules/pairIdSystem.js +++ b/modules/pairIdSystem.js @@ -27,29 +27,29 @@ function pairIdFromCookie(key) { /** @type {Submodule} */ export const pairIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * used to specify vendor id - * @type {number} - */ + * used to specify vendor id + * @type {number} + */ gvlid: 755, /** - * decode the stored id value for passing to bid requests - * @function - * @param { string | undefined } value - * @returns {{pairId:string} | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { string | undefined } value + * @returns {{pairId:string} | undefined } + */ decode(value) { return value && Array.isArray(value) ? {'pairId': value} : undefined }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @returns {id: string | undefined } - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @returns {id: string | undefined } + */ getId(config) { const pairIdsString = pairIdFromLocalStorage(PAIR_ID_KEY) || pairIdFromCookie(PAIR_ID_KEY) let ids = [] diff --git a/modules/pangleBidAdapter.js b/modules/pangleBidAdapter.js new file mode 100644 index 00000000000..f4a52168743 --- /dev/null +++ b/modules/pangleBidAdapter.js @@ -0,0 +1,178 @@ +// 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'; +import { deepSetValue, generateUUID, timestamp, deepAccess } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +import { Renderer } from '../src/Renderer.js'; + +const BIDDER_CODE = 'pangle'; +const ENDPOINT = 'https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'; + +const OUTSTREAM_RENDERER_URL = 'https://sf16-static.i18n-pglstatp.com/obj/ad-pattern-sg/pangle/web/ads/video.js'; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const PANGLE_COOKIE = '_pangle_id'; +const COOKIE_EXP = 86400 * 1000 * 365 * 1; // 1 year +const MEDIA_TYPES = { + Banner: 1, + Video: 2 +}; + +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: BIDDER_CODE }) + +export function isValidUuid(uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getPangleCookieId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(PANGLE_COOKIE); + + if (!sid || !isValidUuid(sid)) { + sid = generateUUID(); + setPangleCookieId(sid); + } + + return sid; +} + +function setPangleCookieId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(timestamp() + COOKIE_EXP).toGMTString(); + + storage.setCookie(PANGLE_COOKIE, sid, expires); + } +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }); + const devicetype = spec.getDeviceType(navigator.userAgent); + deepSetValue(data, 'device.devicetype', devicetype); + if (bidderRequest.userId && typeof bidderRequest.userId === 'object') { + const pangleId = getPangleCookieId(); + // add pangle cookie + const _eids = data.user?.ext?.eids ?? []; + deepSetValue(data, 'user.ext.eids', [ + ..._eids, + { + source: document.location.host, + uids: [ + { + id: pangleId, + atype: 1, + }, + ], + }, + ]); + } + bidRequests.forEach((item, idx) => { + deepSetValue(data.imp[idx], 'ext.networkids', item.params); + deepSetValue(data.imp[idx], 'banner.api', [5]); + deepSetValue(data, 'test', item.params.test ?? 0) + }); + return { + method: 'POST', + url: ENDPOINT, + data, + options: { contentType: 'application/json', withCredentials: true } + } +} + +function isVideoBid(bid) { + return !!deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return !!deepAccess(bid, 'mediaTypes.banner'); +} + +function renderOutstream(bid) { + bid.renderer.push(() => { + window.outstreamPlayer({ bid, codeId: bid.adUnitCode }); + }); +} + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY, + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + let bidResponse; + if (bid.mtype === MEDIA_TYPES.Video) { + context.mediaType = VIDEO; + bidResponse = buildBidResponse(bid, context); + if (bidRequest.mediaTypes.video?.context === 'outstream') { + const renderer = Renderer.install({id: bid.bidId, url: OUTSTREAM_RENDERER_URL, adUnitCode: bid.adUnitCode}); + renderer.setRender(renderOutstream); + bidResponse.renderer = renderer; + } + } + if (bid.mtype === MEDIA_TYPES.Banner) { + context.mediaType = BANNER; + bidResponse = buildBidResponse(bid, context); + } + return bidResponse; + }, +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + getDeviceType: function (ua) { + if ( + /ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test( + ua.toLowerCase() + ) + ) { + return 5; // 'tablet' + } + if ( + /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( + ua.toLowerCase() + ) + ) { + return 4; // 'mobile' + } + return 2; // 'desktop' + }, + + isBidRequestValid: function (bid) { + return Boolean(bid.params.token); + }, + + buildRequests(bidRequests, bidderRequest) { + const videoBids = bidRequests.filter((bid) => isVideoBid(bid)); + const bannerBids = bidRequests.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; + }, + + interpretResponse(response, request) { + const bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids; + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/pangleBidAdapter.md b/modules/pangleBidAdapter.md new file mode 100644 index 00000000000..8fc628dcc89 --- /dev/null +++ b/modules/pangleBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: pangle Bidder Adapter +Module Type: Bidder Adapter +Maintainer: + +# Description + +An adapter to get a bid from pangle DSP. + +# Test Parameters + +```javascript +var adUnits = [{ + // banner + code: 'test1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + bids: [{ + bidder: 'pangle', + params: { + token: 'aass', + appid: 612, + placementid: 123, + } + }] +}]; +``` diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js new file mode 100644 index 00000000000..b38131c1c18 --- /dev/null +++ b/modules/pgamsspBidAdapter.js @@ -0,0 +1,229 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pgamssp'; +const AD_URL = 'https://us-east.pgammedia.com/pbjs'; +const SYNC_URL = 'https://cs.pgammedia.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor, + eids: [] + }; + + if (bid.userId) { + getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + } + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/pgamsspBidAdapter.md b/modules/pgamsspBidAdapter.md new file mode 100644 index 00000000000..c162ec33053 --- /dev/null +++ b/modules/pgamsspBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PGAMSSP Bidder Adapter +Module Type: PGAMSSP Bidder Adapter +Maintainer: info@pgammedia.com +``` + +# Description + +Connects to PGAMSSP exchange for bids. +PGAMSSP bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/pilotxBidAdapter.js b/modules/pilotxBidAdapter.js index 335c461e3d9..bd3612d6429 100644 --- a/modules/pilotxBidAdapter.js +++ b/modules/pilotxBidAdapter.js @@ -6,11 +6,11 @@ export const spec = { supportedMediaTypes: ['banner', 'video'], aliases: ['pilotx'], // short code /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function (bid) { let sizesCheck = !!bid.sizes let paramSizesCheck = !!bid.params.sizes @@ -35,11 +35,11 @@ export const spec = { return !!(bid.params.placementId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function (validBidRequests, bidderRequest) { let payloadItems = {}; validBidRequests.forEach(bidRequest => { @@ -84,11 +84,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function (serverResponse, bidRequest) { const serverBody = serverResponse.body; const bidResponses = []; diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 608ba20aa5f..1c3f9b8da1a 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -3,10 +3,11 @@ import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find, includes} from '../src/polyfill.js'; -import {convertCamelToUnderscore, deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; import {auctionManager} from '../src/auctionManager.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const SOURCE = 'pbjs'; const storageManager = getStorageManager({bidderCode: 'pixfuture'}); diff --git a/modules/prebidServerBidAdapter/config.js b/modules/prebidServerBidAdapter/config.js index f6b8ac9f86a..87274504f64 100644 --- a/modules/prebidServerBidAdapter/config.js +++ b/modules/prebidServerBidAdapter/config.js @@ -1,11 +1,11 @@ // accountId and bidders params are not included here, should be configured by end-user export const S2S_VENDORS = { - 'appnexus': { + 'appnexuspsp': { adapter: 'prebidServer', enabled: true, endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' + p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', + noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }, syncEndpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', @@ -13,15 +13,6 @@ export const S2S_VENDORS = { }, timeout: 1000 }, - 'appnexuspsp': { - adapter: 'prebidServer', - enabled: true, - endpoint: { - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', - noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' - }, - timeout: 1000 - }, 'rubicon': { adapter: 'prebidServer', enabled: true, @@ -47,5 +38,14 @@ export const S2S_VENDORS = { noP1Consent: 'https://prebid.openx.net/cookie_sync' }, timeout: 1000 + }, + 'openwrap': { + adapter: 'prebidServer', + enabled: true, + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + timeout: 500 } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3cae4497354..5ae9a9fc27a 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,6 +1,6 @@ import Adapter from '../../src/adapter.js'; import { - bind, + deepAccess, deepClone, flatten, generateUUID, @@ -15,7 +15,6 @@ import { logWarn, triggerPixel, uniques, - deepAccess, } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; @@ -208,7 +207,7 @@ getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); /** * resets the _synced variable back to false, primiarily used for testing purposes -*/ + */ export function resetSyncedStatus() { _syncCount = 0; } @@ -297,7 +296,7 @@ function doAllSyncs(bidders, s2sConfig) { // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { - doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); + doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, doAllSyncs.bind(null, bidders, s2sConfig), s2sConfig); } else { // bidder already has an ID, so just recurse to the next sync entry doAllSyncs(bidders, s2sConfig); @@ -356,8 +355,7 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { if (clientAdapter && clientAdapter.registerSyncs) { config.runWithBidder( bidder, - bind.call( - clientAdapter.registerSyncs, + clientAdapter.registerSyncs.bind( clientAdapter, [], gdprConsent, @@ -483,7 +481,11 @@ export function PrebidServer() { done(); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, - onError: done, + onError(msg, error) { + logError(`Prebid server call failed: '${msg}'`, error); + bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, {error, bidderRequest})); + done(); + }, onBid: function ({adUnit, bid}) { const metrics = bid.metrics = s2sBidRequest.metrics.fork().renameWith(); metrics.checkpoint('addBidResponse'); @@ -501,8 +503,8 @@ export function PrebidServer() { } } }, - onFledge: ({adUnitCode, config}) => { - addComponentAuction(adUnitCode, config); + onFledge: (params) => { + addComponentAuction({auctionId: bidRequests[0].auctionId, ...params}, params.config); } }) } @@ -591,7 +593,7 @@ function shouldEmitNonbids(s2sConfig, response) { /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included - * @param {array} newEidPermissions + * @param {Array} newEidPermissions */ function setEidPermissions(newEidPermissions) { eidPermissions = newEidPermissions; diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 9d29b66cfc8..7aeb4302280 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -17,13 +17,14 @@ import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'; import {setImpBidParams} from '../../libraries/pbsExtensions/processors/params.js'; import {SUPPORTED_MEDIA_TYPES} from '../../libraries/pbsExtensions/processors/mediaType.js'; import {IMP, REQUEST, RESPONSE} from '../../src/pbjsORTB.js'; -import {beConvertCurrency} from '../../src/utils/currency.js'; import {redactor} from '../../src/activities/redactor.js'; import {s2sActivityParams} from '../../src/adapterManager.js'; import {activityParams} from '../../src/activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js'; +import {currencyCompare} from '../../libraries/currencyUtils/currency.js'; +import {minimum} from '../../src/utils/reducers.js'; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; @@ -141,6 +142,7 @@ const PBS_CONVERTER = ortbConverter({ bidfloor(orig, imp, proxyBidRequest, context) { // for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing, // and aggregate all of them into a single, minimum floor to put in the request + const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); let min; for (const req of context.actualBidRequests.values()) { const floor = {}; @@ -149,14 +151,8 @@ const PBS_CONVERTER = ortbConverter({ if (floor.bidfloorcur == null || floor.bidfloor == null) { min = null; break; - } else if (min == null) { - min = floor; - } else { - const value = beConvertCurrency(floor.bidfloor, floor.bidfloorcur, min.bidfloorcur); - if (value != null && value < min.bidfloor) { - min = floor; - } } + min = min == null ? floor : getMin(min, floor); } if (min != null) { Object.assign(imp, min); @@ -168,6 +164,10 @@ const PBS_CONVERTER = ortbConverter({ // FPD is handled different for PBS - the base request will only contain global FPD; // bidder-specific values are set in ext.prebid.bidderconfig + if (context.transmitTids) { + deepSetValue(ortbRequest, 'source.tid', proxyBidderRequest.auctionId); + } + mergeDeep(ortbRequest, context.s2sBidRequest.ortb2Fragments?.global); // also merge in s2sConfig.extPrebid @@ -240,7 +240,16 @@ const PBS_CONVERTER = ortbConverter({ }, fledgeAuctionConfigs(orig, response, ortbResponse, context) { const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({adUnitCode: impCtx.adUnit.code, config: cfg.config}))); + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => { + const bidderReq = impCtx.actualBidderRequests.find(br => br.bidderCode === cfg.bidder); + const bidReq = impCtx.actualBidRequests.get(cfg.bidder); + return { + adUnitCode: impCtx.adUnit.code, + ortb2: bidderReq?.ortb2, + ortb2Imp: bidReq?.ortb2Imp, + config: cfg.config + }; + })); if (configs.length > 0) { response.fledgeAuctionConfigs = configs; } diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js index b62d8b0c6b4..9125f6f3911 100644 --- a/modules/precisoBidAdapter.js +++ b/modules/precisoBidAdapter.js @@ -1,14 +1,15 @@ -import { logMessage } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'preciso'; const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; -const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=preciso_srl'; +const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const GVLID = 874; +let userId = 'NA'; export const spec = { code: BIDDER_CODE, @@ -22,9 +23,17 @@ export const spec = { buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - + // userId = validBidRequests[0].userId.pubcid; let winTop = window; let location; + var offset = new Date().getTimezoneOffset(); + logInfo('timezone ' + offset); + var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + logInfo('location test' + city) + + const countryCode = getCountryCodeByTimezone(city); + logInfo(`The country code for ${city} is ${countryCode}`); + // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin try { location = new URL(bidderRequest.refererInfo.page) @@ -33,26 +42,57 @@ export const spec = { location = winTop.location; logMessage(e); }; - let placements = []; - let imp = []; - let site = { - 'domain': location.domain || '', - 'page': location || '' - } let request = { - 'id': '123456', - 'imp': imp, - 'site': site, + id: validBidRequests[0].bidderRequestId, + + imp: validBidRequests.map(request => { + const { bidId, sizes, mediaType, ortb2 } = request + const item = { + id: bidId, + region: request.params.region, + traffic: mediaType, + bidFloor: getBidFloor(request), + ortb2: ortb2 + + } + + if (request.mediaTypes.banner) { + item.banner = { + format: (request.mediaTypes.banner.sizes || sizes).map(size => { + return { w: size[0], h: size[1] } + }), + } + } + + if (request.schain) { + item.schain = request.schain; + } + + if (request.floorData) { + item.bidFloor = request.floorData.floorMin; + } + return item + }), + auctionId: validBidRequests[0].auctionId, 'deviceWidth': winTop.screen.width, 'deviceHeight': winTop.screen.height, 'language': (navigator && navigator.language) ? navigator.language : '', - 'secure': 1, + geo: navigator.geolocation.getCurrentPosition(position => { + const { latitude, longitude } = position.coords; + return { + latitude: latitude, + longitude: longitude + } + // Show a map centered at latitude / longitude. + }) || { utcoffset: new Date().getTimezoneOffset() }, + city: city, 'host': location.host, 'page': location.pathname, - 'coppa': config.getConfig('coppa') === true ? 1 : 0, - 'placements': placements + 'coppa': config.getConfig('coppa') === true ? 1 : 0 + // userId: validBidRequests[0].userId }; + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) if (bidderRequest) { if (bidderRequest.uspConsent) { @@ -66,32 +106,11 @@ export const spec = { } } - const len = validBidRequests.length; - - for (let i = 0; i < len; i++) { - let bid = validBidRequests[i]; - let traff = bid.params.traffic || BANNER - placements.push({ - region: bid.params.region, - bidId: bid.bidId, - sizes: bid.mediaTypes && bid.mediaTypes[traff] && bid.mediaTypes[traff].sizes ? bid.mediaTypes[traff].sizes : [], - traffic: traff, - publisherId: bid.params.publisherId - }); - imp.push({ - id: bid.bidId, - sizes: bid.mediaTypes && bid.mediaTypes[traff] && bid.mediaTypes[traff].sizes ? bid.mediaTypes[traff].sizes : [], - traffic: traff, - publisherId: bid.params.publisherId - }) - if (bid.schain) { - placements.schain = bid.schain; - } - } return { method: 'POST', url: AD_URL, - data: request + data: request, + }; }, @@ -126,10 +145,13 @@ export const spec = { let syncs = []; let { gdprApplies, consentString = '' } = gdprConsent; + if (serverResponses.length > 0) { + logInfo('preciso bidadapter getusersync serverResponses:' + serverResponses.toString); + } if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=4` + url: `${URL_SYNC}id=${userId}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=4` }); } else { syncs.push({ @@ -143,4 +165,48 @@ export const spec = { }; +function getCountryCodeByTimezone(city) { + try { + const now = new Date(); + const options = { + timeZone: city, + timeZoneName: 'long', + }; + const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) + .formatToParts(now) + .filter((part) => part.type === 'timeZoneName'); + + if (timeZoneName) { + // Extract the country code from the timezone name + const parts = timeZoneName.value.split('-'); + if (parts.length >= 2) { + return parts[1]; + } + } + } catch (error) { + // Handle errors, such as an invalid timezone city + logInfo(error); + } + + // Handle the case where the city is not found or an error occurred + return 'Unknown'; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + registerBidder(spec); diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 37167fff691..9c3869c480a 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -4,7 +4,6 @@ import { deepClone, deepSetValue, generateUUID, - getGptSlotInfoForAdUnitCode, getParameterByName, isNumber, logError, @@ -13,7 +12,8 @@ import { mergeDeep, parseGPTSingleSizeArray, parseUrl, - pick + pick, + deepEqual } from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {config} from '../src/config.js'; @@ -27,8 +27,9 @@ import {bidderSettings} from '../src/bidderSettings.js'; import {auctionManager} from '../src/auctionManager.js'; import {IMP, PBS, registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js'; -import {beConvertCurrency} from '../src/utils/currency.js'; import {adjustCpm} from '../src/utils/cpm.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertCurrency} from '../libraries/currencyUtils/currency.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -40,19 +41,22 @@ const MODULE_NAME = 'Price Floors'; */ const ajax = ajaxBuilder(10000); +// eslint-disable-next-line symbol-description +const SYN_FIELD = Symbol(); + /** * @summary Allowed fields for rules to have */ -export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; +export let allowedFields = [SYN_FIELD, 'gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; /** * @summary This is a flag to indicate if a AJAX call is processing for a floors request -*/ + */ let fetching = false; /** * @summary so we only register for our hooks once -*/ + */ let addedFloorsHook = false; /** @@ -104,6 +108,7 @@ function getAdUnitCode(request, response, {index = auctionManager.index} = {}) { * @summary floor field types with their matching functions to resolve the actual matched value */ export let fieldMatchingFunctions = { + [SYN_FIELD]: () => '*', 'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*', 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, @@ -117,6 +122,7 @@ export let fieldMatchingFunctions = { * Returns array of Tuple [exact match, catch all] for each field in rules file */ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { + if (!floorFields.length) return []; // generate combination of all exact matches and catch all for each field type return floorFields.reduce((accum, field) => { let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*'; @@ -132,7 +138,9 @@ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { */ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) { let fieldValues = enumeratePossibleFieldValues(deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); - if (!fieldValues.length) return { matchingFloor: floorData.default }; + if (!fieldValues.length) { + return {matchingFloor: undefined} + } // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); @@ -146,9 +154,9 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], + floorRuleValue: floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters - matchingRule + matchingRule: matchingRule === floorData.meta?.defaultRule ? undefined : matchingRule }; // use adUnit floorMin as priority! const floorMin = deepAccess(bidObject, 'ortb2Imp.ext.prebid.floors.floorMin'); @@ -300,14 +308,20 @@ function normalizeRulesForAuction(floorData, adUnitCode) { * Only called if no set config or fetch level data has returned */ export function getFloorDataFromAdUnits(adUnits) { + const schemaAu = adUnits.find(au => au.floors?.schema != null); return adUnits.reduce((accum, adUnit) => { - if (isFloorsDataValid(adUnit.floors)) { + if (adUnit.floors?.schema != null && !deepEqual(adUnit.floors.schema, schemaAu?.floors?.schema)) { + logError(`${MODULE_NAME}: adUnit '${adUnit.code}' declares a different schema from one previously declared by adUnit '${schemaAu.code}'. Floor config for '${adUnit.code}' will be ignored.`) + return accum; + } + const floors = Object.assign({}, schemaAu?.floors, {values: undefined}, adUnit.floors) + if (isFloorsDataValid(floors)) { // if values already exist we want to not overwrite them if (!accum.values) { - accum = getFloorsDataForAuction(adUnit.floors, adUnit.code); + accum = getFloorsDataForAuction(floors, adUnit.code); accum.location = 'adUnit'; } else { - let newRules = getFloorsDataForAuction(adUnit.floors, adUnit.code).values; + let newRules = getFloorsDataForAuction(floors, adUnit.code).values; // copy over the new rules into our values object Object.assign(accum.values, newRules); } @@ -318,13 +332,29 @@ export function getFloorDataFromAdUnits(adUnits) { }, {}); } +function getNoFloorSignalBidersArray(floorData) { + const { data, enforcement } = floorData + // The data.noFloorSignalBidders higher priority then the enforcment + if (data?.noFloorSignalBidders?.length > 0) { + return data.noFloorSignalBidders + } else if (enforcement?.noFloorSignalBidders?.length > 0) { + return enforcement.noFloorSignalBidders + } + return [] +} + /** * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction */ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { + const noFloorSignalBiddersArray = getNoFloorSignalBidersArray(floorData) + adUnits.forEach((adUnit) => { adUnit.bids.forEach(bid => { - if (floorData.skipped) { + // check if the bidder is in the no signal list + const isNoFloorSignaled = noFloorSignalBiddersArray.some(bidderName => bidderName === bid.bidder) + if (floorData.skipped || isNoFloorSignaled) { + isNoFloorSignaled && logInfo(`noFloorSignal to ${bid.bidder}`) delete bid.getFloor; } else { bid.getFloor = getFloor; @@ -332,8 +362,9 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { // information for bid and analytics adapters bid.auctionId = auctionId; bid.floorData = { + noFloorSignaled: isNoFloorSignaled, skipped: floorData.skipped, - skipRate: floorData.skipRate, + skipRate: deepAccess(floorData, 'data.skipRate') ?? floorData.skipRate, floorMin: floorData.floorMin, modelVersion: deepAccess(floorData, 'data.modelVersion'), modelWeight: deepAccess(floorData, 'data.modelWeight'), @@ -382,7 +413,7 @@ export function createFloorsDataForAuction(adUnits, auctionId) { resolvedFloorsData.skipped = true; } else { // determine the skip rate now - const auctionSkipRate = getParameterByName('pbjs_skipRate') || resolvedFloorsData.skipRate; + const auctionSkipRate = getParameterByName('pbjs_skipRate') || (deepAccess(resolvedFloorsData, 'data.skipRate') ?? resolvedFloorsData.skipRate); const isSkipped = Math.random() * 100 < parseFloat(auctionSkipRate); resolvedFloorsData.skipped = isSkipped; } @@ -417,7 +448,7 @@ function validateSchemaFields(fields) { if (Array.isArray(fields) && fields.length > 0 && fields.every(field => allowedFields.indexOf(field) !== -1)) { return true; } - logError(`${MODULE_NAME}: Fields recieved do not match allowed fields`); + logError(`${MODULE_NAME}: Fields received do not match allowed fields`); return false; } @@ -443,7 +474,26 @@ function validateRules(floorsData, numFields, delimiter) { return Object.keys(floorsData.values).length > 0; } +export function normalizeDefault(model) { + if (isNumber(model.default)) { + let defaultRule = '*'; + const numFields = (model.schema?.fields || []).length; + if (!numFields) { + deepSetValue(model, 'schema.fields', [SYN_FIELD]); + } else { + defaultRule = Array(numFields).fill('*').join(model.schema?.delimiter || '|'); + } + model.values = model.values || {}; + if (model.values[defaultRule] == null) { + model.values[defaultRule] = model.default; + model.meta = {defaultRule}; + } + } + return model; +} + function modelIsValid(model) { + model = normalizeDefault(model); // schema.fields has only allowed attributes if (!validateSchemaFields(deepAccess(model, 'schema.fields'))) { return false; @@ -583,7 +633,7 @@ function handleFetchError(status) { } /** - * This function handles sending and recieving the AJAX call for a floors fetch + * This function handles sending and receiving the AJAX call for a floors fetch * @param {object} floorsConfig the floors config coming from setConfig */ export function generateAndHandleFetch(floorEndpoint) { @@ -630,7 +680,8 @@ export function handleSetFloorsConfig(config) { 'enforceJS', enforceJS => enforceJS !== false, // defaults to true 'enforcePBS', enforcePBS => enforcePBS === true, // defaults to false 'floorDeals', floorDeals => floorDeals === true, // defaults to false - 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true + 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true, + 'noFloorSignalBidders', noFloorSignalBidders => noFloorSignalBidders || [] ]), 'additionalSchemaFields', additionalSchemaFields => typeof additionalSchemaFields === 'object' && Object.keys(additionalSchemaFields).length > 0 ? addFieldOverrides(additionalSchemaFields) : undefined, 'data', data => (data && parseFloorData(data, 'setConfig')) || undefined @@ -715,7 +766,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a let floorInfo = getFirstMatchingFloor(floorData.data, matchingBidRequest, {...bid, size: [bid.width, bid.height]}); if (!floorInfo.matchingFloor) { - logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); + if (floorInfo.matchingFloor !== 0) logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); return fn.call(this, adUnitCode, bid, reject); } @@ -794,8 +845,8 @@ export function setImpExtPrebidFloors(imp, bidRequest, context) { if (floorMinCur == null) { floorMinCur = imp.bidfloorcur } const ortb2ImpFloorCur = imp.ext?.prebid?.floors?.floorMinCur || imp.ext?.prebid?.floorMinCur || floorMinCur; const ortb2ImpFloorMin = imp.ext?.prebid?.floors?.floorMin || imp.ext?.prebid?.floorMin; - const convertedFloorMinValue = beConvertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); - const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? beConvertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; + const convertedFloorMinValue = convertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); + const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? convertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; const lowestImpFloorMin = convertedOrtb2ImpFloorMinValue && convertedOrtb2ImpFloorMinValue < convertedFloorMinValue ? convertedOrtb2ImpFloorMinValue diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index 7c9108f60b1..931c5e7d7e6 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -2,7 +2,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; const BIDDER_CODE = 'prisma'; const BIDDER_URL = 'https://prisma.nexx360.io/prebid'; @@ -44,20 +44,20 @@ export const spec = { aliases: ['prismadirect'], // short code supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { return !!(bid.params.account && bid.params.tagId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const adUnits = []; const test = config.getConfig('debug') ? 1 : 0; @@ -109,11 +109,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { const serverBody = serverResponse.body; const bidResponses = []; @@ -163,12 +163,12 @@ export const spec = { }, /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { @@ -179,9 +179,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction - */ + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid const params = { type: 'prebid', mediatype: 'banner' }; diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js new file mode 100644 index 00000000000..7d52e305189 --- /dev/null +++ b/modules/programmaticaBidAdapter.js @@ -0,0 +1,153 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; + +const BIDDER_CODE = 'programmatica'; +const DEFAULT_ENDPOINT = 'asr.programmatica.com'; +const SYNC_ENDPOINT = 'sync.programmatica.com'; +const ADOMAIN = 'programmatica.com'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + let valid = bid.params.siteId && bid.params.placementId; + + return !!valid; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + for (const bid of validBidRequests) { + let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT; + + requests.push({ + method: 'GET', + url: `https://${endpoint}/get`, + data: { + site_id: bid.params.siteId, + placement_id: bid.params.placementId, + prebid: true, + }, + bidRequest: bid, + }); + } + + return requests; + }, + + interpretResponse: function(serverResponse, request) { + if (!serverResponse?.body?.content?.data) { + return []; + } + + const bidResponses = []; + const body = serverResponse.body; + + let mediaType = BANNER; + let ad, vastXml; + let width; + let height; + + let sizes = getSize(body.size); + if (isArray(sizes)) { + [width, height] = sizes; + } + + if (body.type.format != '') { + // banner + ad = body.content.data; + if (body.content.imps?.length) { + for (const imp of body.content.imps) { + ad += ``; + } + } + } else { + // video + vastXml = body.content.data; + mediaType = VIDEO; + + if (!width || !height) { + const pSize = deepAccess(request.bidRequest, 'mediaTypes.video.playerSize'); + const reqSize = getSize(pSize); + if (isArray(reqSize)) { + [width, height] = reqSize; + } + } + } + + const bidResponse = { + requestId: request.bidRequest.bidId, + cpm: body.cpm, + currency: body.currency || 'USD', + width: parseInt(width), + height: parseInt(height), + creativeId: body.id, + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: ad, + mediaType: mediaType, + vastXml: vastXml, + meta: { + advertiserDomains: [ADOMAIN], + } + }; + + if ((mediaType === VIDEO && request.bidRequest.mediaTypes?.video) || (mediaType === BANNER && request.bidRequest.mediaTypes?.banner)) { + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + if (!hasPurpose1Consent(gdprConsent)) { + return syncs; + } + + let params = `usp=${uspConsent ?? ''}&consent=${gdprConsent?.consentString ?? ''}`; + if (typeof gdprConsent?.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}` + }); + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${SYNC_ENDPOINT}/match/sp?${params}` + }); + } + + return syncs; + }, + + onTimeout: function(timeoutData) {}, + onBidWon: function(bid) {}, + onSetTargeting: function(bid) {}, + onBidderError: function() {}, + supportedMediaTypes: [ BANNER, VIDEO ] +} + +registerBidder(spec); + +function getSize(paramSizes) { + const parsedSizes = parseSizesInput(paramSizes); + const sizes = parsedSizes.map(size => { + const [width, height] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return [w, h]; + }); + + return sizes[0] || null; +} diff --git a/modules/programmaticaBidAdapter.md b/modules/programmaticaBidAdapter.md new file mode 100644 index 00000000000..5982edf143e --- /dev/null +++ b/modules/programmaticaBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: Programmatica Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@programmatica.com +``` + +# Description +Connects to Programmatica server for bids. +Module supports banner and video mediaType. + +# Test Parameters + +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cgim20sipgj0vj1cb510' + } + }] + }, + { + code: '/test/div', + mediaTypes: { + video: { + playerSize: [[640, 360]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cioghpcipgj8r721e9ag' + } + }] + },]; +``` diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 5b20dbb620a..1a993c99b45 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -16,6 +16,8 @@ const MODULE_NAME = 'publinkId'; const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; +const PUBLINK_REQUEST_PATH = '/cvx/client/sync/publink'; +const PUBLINK_REFRESH_PATH = '/cvx/client/sync/publink/refresh'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -23,10 +25,9 @@ function isHex(s) { return /^[A-F0-9]+$/i.test(s); } -function publinkIdUrl(params, consentData) { - let url = parseUrl('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink'); +function publinkIdUrl(params, consentData, storedId) { + let url = parseUrl('https://proc.ad.cpe.dotomi.com' + PUBLINK_REFRESH_PATH); url.search = { - deh: params.e, mpn: 'Prebid.js', mpv: '$prebid.version$', }; @@ -36,9 +37,21 @@ function publinkIdUrl(params, consentData) { url.search.gdpr_consent = consentData.consentString; } - if (params.site_id) { url.search.sid = params.site_id; } + if (params) { + if (params.e) { + // if there's an email parameter call the request path + url.search.deh = params.e; + url.pathname = PUBLINK_REQUEST_PATH; + } + + if (params.site_id) { url.search.sid = params.site_id; } + + if (params.api_key) { url.search.apikey = params.api_key; } + } - if (params.api_key) { url.search.apikey = params.api_key; } + if (storedId) { + url.search.publink = storedId; + } const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString && typeof usPrivacyString === 'string') { @@ -48,7 +61,7 @@ function publinkIdUrl(params, consentData) { return buildUrl(url); } -function makeCallback(config = {}, consentData) { +function makeCallback(config = {}, consentData, storedId) { return function(prebidCallback) { const options = {method: 'GET', withCredentials: true}; let handleResponse = function(responseText, xhr) { @@ -59,15 +72,12 @@ function makeCallback(config = {}, consentData) { } } }; - - if (config.params && config.params.e) { - if (isHex(config.params.e)) { - ajax(publinkIdUrl(config.params, consentData), handleResponse, undefined, options); - } else { - logError('params.e must be a hex string'); - } + if ((config.params && config.params.e && isHex(config.params.e)) || storedId) { + ajax(publinkIdUrl(config.params, consentData, storedId), handleResponse, undefined, options); + } else if (config.params.e) { + logError('params.e must be a hex string'); } - }; + } } function getlocalValue() { @@ -137,9 +147,7 @@ export const publinkIdSubmodule = { if (localValue) { return {id: localValue}; } - if (!storedId) { - return {callback: makeCallback(config, consentData)}; - } + return {callback: makeCallback(config, consentData, storedId)}; }, eids: { 'publinkId': { diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index 3dcabb32a3d..ad2a06ea86d 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -1,13 +1,15 @@ -import { _each, pick, logWarn, isStr, isArray, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; +import {_each, isArray, isStr, logError, logWarn, pick, generateUUID} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /// /////////// CONSTANTS ////////////// const ADAPTER_CODE = 'pubmatic'; +const VENDOR_OPENWRAP = 'openwrap'; const SEND_TIMEOUT = 2000; const END_POINT_HOST = 'https://t.pubmatic.com/'; const END_POINT_BID_LOGGER = END_POINT_HOST + 'wl?'; @@ -22,6 +24,7 @@ const ERROR = 'error'; const REQUEST_ERROR = 'request-error'; const TIMEOUT_ERROR = 'timeout-error'; const EMPTY_STRING = ''; +const OPEN_AUCTION_DEAL_ID = '-1'; const MEDIA_TYPE_BANNER = 'banner'; const CURRENCY_USD = 'USD'; const BID_PRECISION = 2; @@ -91,7 +94,7 @@ function copyRequiredBidDetails(bid) { 'bidderCode', 'adapterCode', 'bidId', - 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out + 'status', () => NO_BID, // default a bid to NO_BID until response is received or bid is timed out 'finalSource as source', 'params', 'floorData', @@ -256,12 +259,27 @@ function isS2SBidder(bidder) { return (s2sBidders.indexOf(bidder) > -1) ? 1 : 0 } +function isOWPubmaticBid(adapterName) { + let s2sConf = config.getConfig('s2sConfig'); + let s2sConfArray = isArray(s2sConf) ? s2sConf : [s2sConf]; + return s2sConfArray.some(conf => { + if (adapterName === ADAPTER_CODE && conf.defaultVendor === VENDOR_OPENWRAP && + conf.bidders.indexOf(ADAPTER_CODE) > -1) { + return true; + } + }) +} + function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { adUnit.bids[bidId].forEach(function(bid) { + let adapterName = getAdapterNameForAlias(bid.adapterCode || bid.bidder); + if (isOWPubmaticBid(adapterName) && isS2SBidder(bid.bidder)) { + return; + } partnerBids.push({ - 'pn': getAdapterNameForAlias(bid.adapterCode || bid.bidder), + 'pn': adapterName, 'bc': bid.bidderCode || bid.bidder, 'bidid': bid.bidId || bidId, 'db': bid.bidResponse ? 0 : 1, @@ -270,9 +288,10 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, 'en': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, - 'di': bid.bidResponse ? (bid.bidResponse.dealId || EMPTY_STRING) : EMPTY_STRING, + 'di': bid.bidResponse ? (bid.bidResponse.dealId || OPEN_AUCTION_DEAL_ID) : OPEN_AUCTION_DEAL_ID, 'dc': bid.bidResponse ? (bid.bidResponse.dealChannel || EMPTY_STRING) : EMPTY_STRING, - 'l1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, + 'l1': bid.bidResponse ? bid.partnerTimeToRespond : 0, + 'ol1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, 'l2': 0, 'adv': bid.bidResponse ? getAdDomain(bid.bidResponse) || undefined : undefined, 'ss': isS2SBidder(bid.bidder), @@ -283,7 +302,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, - 'frv': (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), + 'frv': bid.bidResponse ? bid.bidResponse.floorData?.floorRuleValue : undefined, 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined }); }); @@ -334,11 +353,11 @@ function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let auctionCache = cache.auctions[auctionId]; - let floorData = auctionCache.floorData; + let wiid = auctionCache?.wiid || auctionId; + let floorData = auctionCache?.floorData; + let floorFetchStatus = getFloorFetchStatus(auctionCache?.floorData); let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; - // will return true if floor data is present. - let fetchStatus = getFloorFetchStatus(auctionCache.floorData); if (!auctionCache) { return; @@ -350,7 +369,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { pixelURL += 'pubid=' + publisherId; outputObj['pubid'] = '' + publisherId; - outputObj['iid'] = '' + auctionId; + outputObj['iid'] = '' + wiid; outputObj['to'] = '' + auctionCache.timeout; outputObj['purl'] = referrer; outputObj['orig'] = getDomainFromUrl(referrer); @@ -359,8 +378,9 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['pdvid'] = '' + profileVersionId; outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = getTgId(); + outputObj['pbv'] = getGlobal()?.version || '-1'; - if (floorData && fetchStatus) { + if (floorData && floorFetchStatus) { outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; } @@ -375,8 +395,25 @@ function executeBidsLoggerCall(e, highestCpmBids) { 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), - 'fskp': (floorData && fetchStatus) ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'fskp': floorData && floorFetchStatus ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'sid': generateUUID() }; + if (floorData?.floorRequestData) { + const { location, fetchStatus, floorProvider } = floorData?.floorRequestData; + slotObject.ffs = { + [CONSTANTS.FLOOR_VALUES.SUCCESS]: 1, + [CONSTANTS.FLOOR_VALUES.ERROR]: 2, + [CONSTANTS.FLOOR_VALUES.TIMEOUT]: 4, + undefined: 0 + }[fetchStatus]; + slotObject.fsrc = { + [CONSTANTS.FLOOR_VALUES.FETCH]: 2, + [CONSTANTS.FLOOR_VALUES.NO_DATA]: 2, + [CONSTANTS.FLOOR_VALUES.AD_UNIT]: 1, + [CONSTANTS.FLOOR_VALUES.SET_CONFIG]: 1 + }[location]; + slotObject.fp = floorProvider; + } slotsArray.push(slotObject); return slotsArray; }, []); @@ -398,16 +435,24 @@ function executeBidsLoggerCall(e, highestCpmBids) { function executeBidWonLoggerCall(auctionId, adUnitId) { const winningBidId = cache.auctions[auctionId].adUnitCodes[adUnitId].bidWon; const winningBids = cache.auctions[auctionId].adUnitCodes[adUnitId].bids[winningBidId]; - let winningBid = winningBids[0]; + if (!winningBids) { + logWarn(LOG_PRE_FIX + 'Could not find winningBids for : ', auctionId); + return; + } + let winningBid = winningBids[0]; if (winningBids.length > 1) { winningBid = winningBids.filter(bid => bid.adId === cache.auctions[auctionId].adUnitCodes[adUnitId].bidWonAdId)[0]; } const adapterName = getAdapterNameForAlias(winningBid.adapterCode || winningBid.bidder); + if (isOWPubmaticBid(adapterName) && isS2SBidder(winningBid.bidder)) { + return; + } let origAdUnit = getAdUnit(cache.auctions[auctionId].origAdUnits, adUnitId) || {}; let 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; @@ -416,7 +461,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += 'pubid=' + publisherId; pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); pixelURL += '&tst=' + Math.round((new window.Date()).getTime() / 1000); - pixelURL += '&iid=' + enc(auctionId); + pixelURL += '&iid=' + enc(wiid); pixelURL += '&bidid=' + enc(winningBidId); pixelURL += '&pid=' + enc(profileId); pixelURL += '&pdvid=' + enc(profileVersionId); @@ -428,6 +473,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); + pixelURL += '&di=' + enc(winningBid?.bidResponse?.dealId || OPEN_AUCTION_DEAL_ID); pixelURL += '&plt=' + enc(getDevicePlatform()); pixelURL += '&psz=' + enc((winningBid?.bidResponse?.dimensions?.width || '0') + 'x' + @@ -456,7 +502,10 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { function auctionInitHandler(args) { s2sBidders = (function() { let s2sConf = config.getConfig('s2sConfig'); - return (s2sConf && isArray(s2sConf.bidders)) ? s2sConf.bidders : []; + let s2sBidders = []; + (s2sConf || []) && + isArray(s2sConf) ? s2sConf.map(conf => s2sBidders.push(...conf.bidders)) : s2sBidders.push(...s2sConf.bidders); + return s2sBidders || []; }()); let cacheEntry = pick(args, [ 'timestamp', @@ -479,6 +528,9 @@ function bidRequestedHandler(args) { dimensions: bid.sizes }; } + if (bid.bidder === 'pubmatic' && !!bid?.params?.wiid) { + cache.auctions[args.auctionId].wiid = bid.params.wiid; + } cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId] = [copyRequiredBidDetails(bid)]; if (bid.floorData) { cache.auctions[args.auctionId].floorData['floorRequestData'] = bid.floorData; @@ -487,6 +539,10 @@ function bidRequestedHandler(args) { } function bidResponseHandler(args) { + if (!args.requestId) { + logWarn(LOG_PRE_FIX + 'Got null requestId in bidResponseHandler'); + return; + } let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId][0]; if (!bid) { logError(LOG_PRE_FIX + 'Could not find associated bid request for bid response with requestId: ', args.requestId); @@ -505,6 +561,10 @@ function bidResponseHandler(args) { bid.adId = args.adId; bid.source = formatSource(bid.source || args.source); setBidStatus(bid, args); + const latency = args?.timeToRespond || Date.now() - cache.auctions[args.auctionId].timestamp; + const auctionTime = cache.auctions[args.auctionId].timeout; + // Check if latency is greater than auctiontime+150, then log auctiontime+150 to avoid large numbers + bid.partnerTimeToRespond = latency > (auctionTime + 150) ? (auctionTime + 150) : latency; bid.clientLatencyTimeMs = Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args); } @@ -547,7 +607,7 @@ function auctionEndHandler(args) { let highestCpmBids = getGlobal().getHighestCpmBids() || []; setTimeout(() => { executeBidsLoggerCall.call(this, args, highestCpmBids); - }, (cache.auctions[args.auctionId].bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); + }, (cache.auctions[args.auctionId]?.bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); } function bidTimeoutHandler(args) { diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index a3833551fff..192c657d976 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -1,10 +1,11 @@ -import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, convertTypes, uniques, isPlainObject, isInteger } from '../src/utils.js'; +import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, uniques, isPlainObject, isInteger, generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE, ADPOD } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { bidderSettings } from '../src/bidderSettings.js'; import CONSTANTS from '../src/constants.json'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -733,9 +734,9 @@ function _addImpressionFPD(imp, bid) { const ortb2 = {...deepAccess(bid, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { /** - * Prebid AdSlot - * @type {(string|undefined)} - */ + * Prebid AdSlot + * @type {(string|undefined)} + */ if (prop === 'pbadslot') { if (typeof ortb2[prop] === 'string' && ortb2[prop]) deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); } else if (prop === 'adserver') { @@ -757,6 +758,9 @@ function _addImpressionFPD(imp, bid) { deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); } }); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + gpid && deepSetValue(imp, `ext.gpid`, gpid); } function _addFloorFromFloorModule(impObj, bid) { @@ -1007,11 +1011,11 @@ export const spec = { gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** - * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: bid => { if (bid && bid.params) { if (!isStr(bid.params.publisherId)) { @@ -1077,8 +1081,10 @@ export const spec = { var bid; var blockedIabCategories = []; var allowedIabCategories = []; + var wiid = generateUUID(); validBidRequests.forEach(originalBid => { + originalBid.params.wiid = originalBid.params.wiid || bidderRequest.auctionId || wiid; bid = deepClone(originalBid); bid.params.adSlot = bid.params.adSlot || ''; _parseAdSlot(bid); @@ -1209,7 +1215,7 @@ export const spec = { // First Party Data const commonFpd = (bidderRequest && bidderRequest.ortb2) || {}; - const { user, device, site, bcat } = commonFpd; + const { user, device, site, bcat, badv } = commonFpd; if (site) { const { page, domain, ref } = payload.site; mergeDeep(payload, {site: site}); @@ -1220,6 +1226,9 @@ export const spec = { if (user) { mergeDeep(payload, {user: user}); } + if (badv) { + mergeDeep(payload, {badv: badv}); + } if (bcat) { blockedIabCategories = blockedIabCategories.concat(bcat); } @@ -1228,6 +1237,10 @@ export const spec = { payload.device.sua = device?.sua; } + if (device?.ext?.cdep) { + deepSetValue(payload, 'device.ext.cdep', device.ext.cdep); + } + if (user?.geo && device?.geo) { payload.device.geo = { ...payload.device.geo, ...device.geo }; payload.user.geo = { ...payload.user.geo, ...user.geo }; @@ -1359,6 +1372,21 @@ export const spec = { }); }); } + let fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return { + bidId, + config: Object.assign({ + auctionSignals: {}, + }, cfg) + } + }); + return { + bids: bidResponses, + fledgeAuctionConfigs, + } + } } catch (error) { logError(error); } diff --git a/modules/pubxBidAdapter.js b/modules/pubxBidAdapter.js index ee28d549475..60e5be2a321 100644 --- a/modules/pubxBidAdapter.js +++ b/modules/pubxBidAdapter.js @@ -55,8 +55,8 @@ export const spec = { /** * Determine which user syncs should occur * @param {object} syncOptions - * @param {array} serverResponses - * @returns {array} User sync pixels + * @param {Array} serverResponses + * @returns {Array} User sync pixels */ getUserSyncs: function (syncOptions, serverResponses) { const kwTag = document.getElementsByName('keywords'); diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 19a3c236942..e97e5505768 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -1,9 +1,10 @@ -import { deepAccess, getGptSlotInfoForAdUnitCode, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; +import { deepAccess, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 7297c931326..516254b358b 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -1,6 +1,7 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import {convertTypes, isArray} from '../src/utils.js'; +import {isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const DEFAULT_CURRENCY = 'USD'; const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; diff --git a/modules/qortexRtdProvider.js b/modules/qortexRtdProvider.js new file mode 100644 index 00000000000..7aa30334756 --- /dev/null +++ b/modules/qortexRtdProvider.js @@ -0,0 +1,165 @@ +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { logWarn, mergeDeep, logMessage, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +let requestUrl; +let bidderArray; +let impressionIds; +let currentSiteContext; + +/** + * Init if module configuration is valid + * @param {Object} config Module configuration + * @returns {Boolean} + */ +function init (config) { + if (!config?.params?.groupId?.length > 0) { + logWarn('Qortex RTD module config does not contain valid groupId parameter. Config params: ' + JSON.stringify(config.params)) + return false; + } else { + initializeModuleData(config); + } + if (config?.params?.tagConfig) { + loadScriptTag(config) + } + return true; +} + +/** + * Processess prebid request and attempts to add context to ort2b fragments + * @param {Object} reqBidsConfig Bid request configuration object + * @param {Function} callback Called on completion + */ +function getBidRequestData (reqBidsConfig, callback) { + if (reqBidsConfig?.adUnits?.length > 0) { + getContext() + .then(contextData => { + setContextData(contextData) + addContextToRequests(reqBidsConfig) + callback(); + }) + .catch((e) => { + logWarn(e?.message); + callback(); + }); + } else { + logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig)) + callback(); + } +} + +/** + * determines whether to send a request to context api and does so if necessary + * @returns {Promise} ortb Content object + */ +export function getContext () { + if (!currentSiteContext) { + logMessage('Requesting new context data'); + return new Promise((resolve, reject) => { + const callbacks = { + success(text, data) { + const result = data.status === 200 ? JSON.parse(data.response)?.content : null; + resolve(result); + }, + error(error) { + reject(new Error(error)); + } + } + ajax(requestUrl, callbacks) + }) + } else { + logMessage('Adding Content object from existing context data'); + return new Promise(resolve => resolve(currentSiteContext)); + } +} + +/** + * Updates bidder configs with the response from Qortex context services + * @param {Object} reqBidsConfig Bid request configuration object + * @param {string[]} bidders Bidders specified in module's configuration + */ +export function addContextToRequests (reqBidsConfig) { + if (currentSiteContext === null) { + logWarn('No context data received at this time'); + } else { + const fragment = { site: {content: currentSiteContext} } + if (bidderArray?.length > 0) { + bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment})) + } else if (!bidderArray) { + mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment); + } else { + logWarn('Config contains an empty bidders array, unable to determine which bids to enrich'); + } + } +} + +/** + * Loads Qortex header tag using data passed from module config object + * @param {Object} config module config obtained during init + */ +export function loadScriptTag(config) { + const code = 'qortex'; + const groupId = config.params.groupId; + const src = 'https://tags.qortex.ai/bootstrapper' + const attr = {'data-group-id': groupId} + const tc = config.params.tagConfig + + Object.keys(tc).forEach(p => { + attr[`data-${p.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`)}`] = tc[p] + }) + + addEventListener('qortex-rtd', (e) => { + const billableEvent = { + vendor: code, + billingId: generateUUID(), + type: e?.detail?.type, + accountId: groupId + } + switch (e?.detail?.type) { + case 'qx-impression': + const {uid} = e.detail; + if (!uid || impressionIds.has(uid)) { + logWarn(`received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`) + return; + } else { + logMessage('received billable event: qx-impression') + impressionIds.add(uid) + billableEvent.transactionId = e.detail.uid; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, billableEvent); + break; + } + default: + logWarn(`received invalid billable event: ${e.detail?.type}`) + } + }) + + loadExternalScript(src, code, undefined, undefined, attr); +} + +/** + * Helper function to set initial values when they are obtained by init + * @param {Object} config module config obtained during init + */ +export function initializeModuleData(config) { + const DEFAULT_API_URL = 'https://demand.qortex.ai'; + const {apiUrl, groupId, bidders} = config.params; + requestUrl = `${apiUrl || DEFAULT_API_URL}/api/v1/analyze/${groupId}/prebid`; + bidderArray = bidders; + impressionIds = new Set(); + currentSiteContext = null; +} + +export function setContextData(value) { + currentSiteContext = value +} + +export const qortexSubmodule = { + name: 'qortex', + init, + getBidRequestData +} + +submodule('realTimeData', qortexSubmodule); diff --git a/modules/qortexRtdProvider.md b/modules/qortexRtdProvider.md new file mode 100644 index 00000000000..312696068cd --- /dev/null +++ b/modules/qortexRtdProvider.md @@ -0,0 +1,69 @@ +# Qortex Real-time Data Submodule + +## Overview + +``` +Module Name: Qortex RTD Provider +Module Type: RTD Provider +Maintainer: mannese@qortex.ai +``` + +## Description + +The Qortex RTD module appends contextual segments to the bidding object based on the content of a page using the Qortex API. + +Upon load, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data. + + +## Build +``` +gulp build --modules="rtdModule,qortexRtdProvider,qortexBidAdapter,..." +``` + +> `rtdModule` is a required module to use Qortex RTD module. + +## Configuration + +Please refer to [Prebid Documentation](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-realTimeData) on RTD module configuration for details on required and optional parameters of `realTimeData` + +When configuring Qortex as a data provider, refer to the template below to add the necessary information to ensure the proper connection is made. + +### RTD Module Setup + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: 'qortex', + waitForIt: true, + params: { + groupId: 'ABC123', //required + bidders: ['qortex', 'adapter2'], //optional (see below) + tagConfig: { // optional, please reach out to your account manager for configuration reccommendation + videoContainer: 'string', + htmlContainer: 'string', + attachToTop: 'string', + esm6Mod: 'string', + continuousLoad: 'string' + } + } + }] + } +}); +``` + +### Paramter Details + +#### `groupId` - Required +- The Qortex groupId linked to the publisher, this is required to make a request using this adapter + +#### `bidders` - optional +- If this parameter is included, it must be an array of the strings that match the bidder code of the prebid adapters you would like this module to impact. `ortb2.site.content` will be updated *only* for adapters in this array + +- If this parameter is omitted, the RTD module will default to updating `ortb2.site.content` on *all* bid adapters being used on the page + +#### `tagConfig` - optional +- This optional parameter is an object containing the config settings that could be usedto initialize the Qortex integration on your page. A preconfigured object for this step will be provided to you by the Qortex team. + +- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. \ No newline at end of file diff --git a/modules/r2b2BidAdapter.js b/modules/r2b2BidAdapter.js new file mode 100644 index 00000000000..15a65e3924c --- /dev/null +++ b/modules/r2b2BidAdapter.js @@ -0,0 +1,309 @@ +import {logWarn, logError, triggerPixel, deepSetValue, getParameterByName} from '../src/utils.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {Renderer} from '../src/Renderer.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {pbsExtensions} from '../libraries/pbsExtensions/pbsExtensions.js'; +import {bidderSettings} from '../src/bidderSettings.js'; + +const ADAPTER_VERSION = '1.0.0'; +const BIDDER_CODE = 'r2b2'; +const GVL_ID = 1235; + +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TTL = 360; +const DEFAULT_NET_REVENUE = true; +const DEBUG_PARAM = 'pbjs_test_r2b2'; +const RENDERER_URL = 'https://delivery.r2b2.io/static/rendering.js'; + +const ENDPOINT = bidderSettings.get(BIDDER_CODE, 'endpoint') || 'hb.r2b2.cz'; +const SERVER_URL = 'https://' + ENDPOINT; +const URL_BID = SERVER_URL + '/openrtb2/bid'; +const URL_SYNC = SERVER_URL + '/cookieSync'; +const URL_EVENT = SERVER_URL + '/event'; + +const URL_EVENT_ON_BIDDER_ERROR = URL_EVENT + '/bidError'; +const URL_EVENT_ON_TIMEOUT = URL_EVENT + '/timeout'; + +const R2B2_TEST_UNIT = 'selfpromo'; + +export const internal = { + placementsToSync: [], + mappedParams: {} +} + +let r2b2Error = function(message, params) { + logError(message, params, BIDDER_CODE) +} + +function getIdParamsFromPID(pid) { + // selfpromo test creative + if (pid === R2B2_TEST_UNIT) { + return { d: 'test', g: 'test', p: 'selfpromo', m: 0, selfpromo: 1 } + } + if (!isNaN(pid)) { + return { pid: Number(pid) } + } + if (typeof pid === 'string') { + const params = pid.split('/'); + if (params.length === 3 || params.length === 4) { + const paramNames = ['d', 'g', 'p', 'm']; + return paramNames.reduce((p, paramName, index) => { + let param = params[index]; + if (paramName === 'm') { + param = ['desktop', 'classic', '0'].includes(param) ? 0 : Number(!!param) + } + p[paramName] = param; + return p + }, {}); + } + } +} + +function pickIdFromParams(params) { + if (!params) return null; + const { d, g, p, m, pid } = params; + return d ? { d, g, p, m } : { pid }; +} + +function getIdsFromBids(bids) { + return bids.reduce((ids, bid) => { + const params = internal.mappedParams[bid.bidId]; + const id = pickIdFromParams(params); + if (id) { + ids.push(id); + } + return ids + }, []); +} + +function triggerEvent(eventUrl, ids) { + if (ids && !ids.length) return; + const timeStamp = new Date().getTime(); + const symbol = (eventUrl.indexOf('?') === -1 ? '?' : '&'); + const url = eventUrl + symbol + `p=${btoa(JSON.stringify(ids))}&cb=${timeStamp}`; + triggerPixel(url) +} + +const converter = ortbConverter({ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const idParams = getIdParamsFromPID(bidRequest.params.pid); + deepSetValue(imp, 'ext.r2b2', idParams); + internal.placementsToSync.push(idParams); + internal.mappedParams[imp.id] = Object.assign({}, bidRequest.params, idParams); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'ext.version', ADAPTER_VERSION); + request.cur = [DEFAULT_CURRENCY]; + const test = getParameterByName(DEBUG_PARAM) === '1' ? 1 : 0; + deepSetValue(request, 'test', test); + return request; + }, + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL + }, + processors: pbsExtensions +}); + +function setUpRenderer(adUnitCode, bid) { + // let renderer load once in main window, but pass the renderDocument + let renderDoc; + const config = { + documentResolver: (bid, sourceDocument, renderDocument) => { + renderDoc = renderDocument; + return sourceDocument; + } + } + let renderer = Renderer.install({ + url: RENDERER_URL, + config: config, + id: bid.requestId, + adUnitCode + }); + + renderer.setRender(function (bid, doc) { + doc = renderDoc || doc; + window.R2B2 = window.R2B2 || {}; + let main = window.R2B2; + main.HB = main.HB || {}; + main.HB.Render = main.HB.Render || {}; + main.HB.Render.queue = main.HB.Render.queue || []; + main.HB.Render.queue.push(() => { + const id = pickIdFromParams(internal.mappedParams[bid.requestId]) + main.HB.Renderer.render(id, bid, null, doc) + }) + }) + + return renderer +} + +function getExtMediaType(bidMediaType, responseBid) { + switch (bidMediaType) { + case BANNER: + return { + type: 'banner', + settings: { + chd: null, + width: responseBid.w, + height: responseBid.h, + ad: { + type: 'content', + data: responseBid.adm + } + } + }; + case NATIVE: + break; + case VIDEO: + break; + default: + break; + } +} + +function createPrebidResponseBid(requestImp, bidResponse, serverResponse, bids) { + const bidId = requestImp.id; + const adUnitCode = bids[0].adUnitCode; + const mediaType = bidResponse.ext.prebid.type; + let bidOut = { + requestId: bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + width: bidResponse.w, + height: bidResponse.h, + ttl: bidResponse.ttl ?? DEFAULT_TTL, + netRevenue: serverResponse.netRevenue ?? DEFAULT_NET_REVENUE, + currency: serverResponse.cur ?? DEFAULT_CURRENCY, + ad: bidResponse.adm, + mediaType: mediaType, + winUrl: bidResponse.nurl, + ext: { + cid: bidResponse.ext?.r2b2?.cid, + cdid: bidResponse.ext?.r2b2?.cdid, + mediaType: getExtMediaType(mediaType, bidResponse), + adUnit: adUnitCode, + dgpm: internal.mappedParams[bidId], + events: bidResponse.ext?.r2b2?.events + } + }; + if (bidResponse.ext?.r2b2?.useRenderer) { + bidOut.renderer = setUpRenderer(adUnitCode, bidOut); + } + return bidOut; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + if (!bid.params || !bid.params.pid) { + logWarn('Bad params, "pid" required.'); + return false + } + const id = getIdParamsFromPID(bid.params.pid); + if (!id || !(id.pid || (id.d && id.g && id.p))) { + logWarn('Bad params, "pid" has to be either a number or a correctly assembled string.'); + return false + } + return true + }, + buildRequests: function(validBidRequests, bidderRequest) { + const data = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest + }); + return [{ + method: 'POST', + url: URL_BID, + data, + bids: bidderRequest.bids + }] + }, + + interpretResponse: function(serverResponse, request) { + // r2b2Error('error message', {params: 1}); + let prebidResponses = []; + + const response = serverResponse.body; + if (!response || !response.seatbid || !response.seatbid[0] || !response.seatbid[0].bid) { + return prebidResponses; + } + let requestImps = request.data.imp || []; + try { + response.seatbid.forEach(seat => { + let bids = seat.bid; + + for (let responseBid of bids) { + let responseImpId = responseBid.impid; + let requestCurrentImp = requestImps.find((requestImp) => requestImp.id === responseImpId); + if (!requestCurrentImp) { + r2b2Error('Cant match bid response.', {impid: Boolean(responseBid.impid)}); + continue;// Skip this iteration if there's no match + } + prebidResponses.push(createPrebidResponseBid(requestCurrentImp, responseBid, response, request.bids)); + } + }) + } catch (e) { + r2b2Error('Error while interpreting response:', {msg: e.message}); + } + return prebidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + if (!syncOptions.iframeEnabled) { + logWarn('Please enable iframe based user sync.'); + return syncs; + } + + let plString; + try { + plString = btoa(JSON.stringify(internal.placementsToSync || [])); + } catch (e) { + logWarn('User sync failed: ' + e.message); + return syncs + } + + let url = URL_SYNC + `?p=${plString}`; + + if (gdprConsent) { + url += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + } + + if (uspConsent) { + url += `&us_privacy=${uspConsent}` + } + + syncs.push({ + type: 'iframe', + url: url + }) + return syncs; + }, + onBidWon: function(bid) { + const url = bid.ext?.events?.onBidWon; + if (url) { + triggerEvent(url) + } + }, + onSetTargeting: function(bid) { + const url = bid.ext?.events?.onSetTargeting; + if (url) { + triggerEvent(url) + } + }, + onTimeout: function(bids) { + triggerEvent(URL_EVENT_ON_TIMEOUT, getIdsFromBids(bids)) + }, + onBidderError: function(params) { + let { bidderRequest } = params; + triggerEvent(URL_EVENT_ON_BIDDER_ERROR, getIdsFromBids(bidderRequest.bids)) + } +} +registerBidder(spec); diff --git a/modules/r2b2BidAdapter.md b/modules/r2b2BidAdapter.md new file mode 100644 index 00000000000..43b59133215 --- /dev/null +++ b/modules/r2b2BidAdapter.md @@ -0,0 +1,37 @@ +# Overview + +``` +Module Name: R2B2 Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@r2b2.cz +``` + +## Description + +Module that integrates R2B2 demand sources. To get your bidder configuration reach out to our account team on partner@r2b2.io + + + +## Test unit + +```javascript + var adUnits = [ + { + code: 'test-r2b2', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'r2b2', + params: { + pid: 'selfpromo' + } + }] + } + ]; +``` +## Rendering + +Our adapter can feature a custom renderer specifically for display ads, tailored to enhance ad presentation and functionality. This is particularly beneficial for non-standard ad formats that require more complex logic. It's important to note that our rendering process operates outside of SafeFrames. For additional information, not limited to rendering aspects, please feel free to contact us at partner@r2b2.io diff --git a/modules/rasBidAdapter.js b/modules/rasBidAdapter.js index a7aceb107b9..4e93f2aa8eb 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,7 +1,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { isEmpty, getAdUnitSizes, parseSizesInput, deepAccess } from '../src/utils.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'; @@ -129,6 +130,36 @@ const getGdprParams = (bidderRequest) => { return queryString; }; +const parseAuctionConfigs = (serverResponse, bidRequest) => { + if (isEmpty(bidRequest)) { + return null; + } + const auctionConfigs = []; + const gctx = serverResponse && serverResponse.body?.gctx; + + bidRequest.bidIds.filter(bid => bid.fledgeEnabled).forEach((bid) => { + auctionConfigs.push({ + 'bidId': bid.bidId, + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': `https://csr.onet.pl/${encodeURIComponent(bid.params.network)}/v1/protected-audience-api/decision-logic.js`, + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': bid.params, + 'sizes': bid.sizes, + 'gctx': gctx + } + } + }); + }); + + if (auctionConfigs.length === 0) { + return null; + } else { + return auctionConfigs; + } +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], @@ -145,8 +176,16 @@ export const spec = { const slotsQuery = getSlots(bidRequests); const contextQuery = getContextParams(bidRequests, bidderRequest); const gdprQuery = getGdprParams(bidderRequest); - const bidIds = bidRequests.map((bid) => ({ slot: bid.params.slot, bidId: bid.bidId })); + const fledgeEligible = Boolean(bidderRequest && bidderRequest.fledgeEnabled); const network = bidRequests[0].params.network; + const bidIds = bidRequests.map((bid) => ({ + slot: bid.params.slot, + bidId: bid.bidId, + sizes: getAdUnitSizes(bid), + params: bid.params, + fledgeEnabled: fledgeEligible + })); + return [{ method: 'GET', url: getEndpoint(network) + contextQuery + slotsQuery + gdprQuery, @@ -156,10 +195,16 @@ export const spec = { interpretResponse: function (serverResponse, bidRequest) { const response = serverResponse.body; - if (!response || !response.ads || response.ads.length === 0) { - return []; + + const fledgeAuctionConfigs = parseAuctionConfigs(serverResponse, bidRequest); + const bids = (!response || !response.ads || response.ads.length === 0) ? [] : response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); + + if (fledgeAuctionConfigs) { + // Return a tuple of bids and auctionConfigs. It is possible that bids could be null. + return {bids, fledgeAuctionConfigs}; + } else { + return bids; } - return response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); } }; diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index b2961b09eb5..751e8fa442c 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -1,4 +1,14 @@ -import { deepAccess, logWarn, getBidIdParameter, parseQueryStringParameters, triggerPixel, generateUUID, isArray, isNumber, parseSizesInput } from '../src/utils.js'; +import { + deepAccess, + logWarn, + parseQueryStringParameters, + triggerPixel, + generateUUID, + isArray, + isNumber, + parseSizesInput, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; @@ -94,7 +104,8 @@ function buildRequests(validBidRequests, bidderRequest) { width: width, height: height, banner_sizes: getBannerSizes(bidRequest), - media_type: mediaType + media_type: mediaType, + userIdAsEids: bidRequest.userIdAsEids || {}, }); } @@ -107,6 +118,7 @@ function buildRequests(validBidRequests, bidderRequest) { uuid: getUuid(), pv: '$prebid.version$', imuid: imuid, + canonical_url: bidderRequest.refererInfo?.canonicalUrl || null, canonical_url_hash: getCanonicalUrlHash(bidderRequest.refererInfo), ref: bidderRequest.refererInfo.page }); @@ -132,6 +144,7 @@ function interpretResponse(serverResponse, bidRequest) { const playerUrl = res.playerUrl || bidRequest.player || body.playerUrl; let bidResponse = { requestId: res.bidId, + placementId: res.placementId, width: res.width, height: res.height, cpm: res.price, diff --git a/modules/relevantdigitalBidAdapter.js b/modules/relevantdigitalBidAdapter.js index ad9ee5e1e14..8d1265075f9 100644 --- a/modules/relevantdigitalBidAdapter.js +++ b/modules/relevantdigitalBidAdapter.js @@ -94,11 +94,18 @@ export const spec = { gvlid: 1100, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue **/ + /** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue */ isBidRequestValid: (bid) => bid.params?.placementId && getBidderConfig([bid]).complete, /** Trigger impression-pixel */ - onBidWon: ({pbsWurl}) => pbsWurl && triggerPixel(pbsWurl), + onBidWon(bid) { + if (bid.pbsWurl) { + triggerPixel(bid.pbsWurl) + } + if (bid.burl) { + triggerPixel(bid.burl) + } + }, /** Build BidRequest for PBS */ buildRequests(bidRequests, bidderRequest) { @@ -193,6 +200,24 @@ export const spec = { }); return syncs; }, + + /** If server side, transform bid params if needed */ + transformBidParams(params, isOrtb, adUnit, bidRequests) { + if (!params.placementId) { + return; + } + const bid = bidRequests.flatMap(req => req.adUnitsS2SCopy || []).flatMap((adUnit) => adUnit.bids).find((bid) => bid.params?.placementId === params.placementId); + if (!bid) { + return; + } + const cfg = getBidderConfig([bid]); + FIELDS.forEach(({ name }) => { + if (cfg[name] && !params[name]) { + params[name] = cfg[name]; + } + }); + return params; + }, }; registerBidder(spec); diff --git a/modules/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index 8264e0cc9cc..2bac6c6dcba 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -1,25 +1,29 @@ import { timestamp, deepAccess, isStr, deepClone } from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import { config } from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'resetdigital'; const CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [ 'banner', 'video' ], - isBidRequestValid: function(bid) { - return (!!(bid.params.pubId || bid.params.zoneId)); + supportedMediaTypes: ['banner', 'video'], + isBidRequestValid: function (bid) { + return !!(bid.params.pubId || bid.params.zoneId); }, - buildRequests: function(validBidRequests, bidderRequest) { - let stack = (bidderRequest.refererInfo && - bidderRequest.refererInfo.stack ? bidderRequest.refererInfo.stack - : []) - - let spb = (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : 5 + buildRequests: function (validBidRequests, bidderRequest) { + let stack = + bidderRequest.refererInfo && bidderRequest.refererInfo.stack + ? bidderRequest.refererInfo.stack + : []; + + let spb = + config.getConfig('userSync') && + config.getConfig('userSync').syncsPerBidder + ? config.getConfig('userSync').syncsPerBidder + : 5; const payload = { start_time: timestamp(), @@ -29,19 +33,19 @@ export const spec = { iframe: !bidderRequest.refererInfo.reachedTop, // TODO: the last element in refererInfo.stack is window.location.href, that's unlikely to have been the intent here url: stack && stack.length > 0 ? [stack.length - 1] : null, - https: (window.location.protocol === 'https:'), + https: window.location.protocol === 'https:', // TODO: is 'page' the right value here? - referrer: bidderRequest.refererInfo.page + referrer: bidderRequest.refererInfo.page, }, imps: [], user_ids: validBidRequests[0].userId, - sync_limit: spb + sync_limit: spb, }; if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = { applies: bidderRequest.gdprConsent.gdprApplies, - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, }; } @@ -50,10 +54,16 @@ export const spec = { } function getOrtb2Keywords(ortb2Obj) { - const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords']; + const fields = [ + 'site.keywords', + 'site.content.keywords', + 'user.keywords', + 'app.keywords', + 'app.content.keywords', + ]; let result = []; - fields.forEach(path => { + fields.forEach((path) => { let keyStr = deepAccess(ortb2Obj, path); if (isStr(keyStr)) result.push(keyStr); }); @@ -79,18 +89,26 @@ export const spec = { const floorInfo = req.getFloor({ currency: CURRENCY, mediaType: BANNER, - size: '*' + size: '*', }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + if ( + typeof floorInfo === 'object' && + floorInfo.currency === CURRENCY && + !isNaN(parseFloat(floorInfo.floor)) + ) { bidFloor = parseFloat(floorInfo.floor); bidFloorCur = CURRENCY; } } // get param kewords (if it exists) - let paramsKeywords = req.params.keywords ? req.params.keywords.split(',') : []; + let paramsKeywords = req.params.keywords + ? req.params.keywords.split(',') + : []; // merge all keywords - let keywords = ortb2KeywordsList.concat(paramsKeywords).concat(metaKeywords); + let keywords = ortb2KeywordsList + .concat(paramsKeywords) + .concat(metaKeywords); payload.imps.push({ pub_id: req.params.pubId, @@ -110,32 +128,32 @@ export const spec = { sizes: req.sizes, force_bid: req.params.forceBid, coppa: config.getConfig('coppa') === true ? 1 : 0, - media_types: deepAccess(req, 'mediaTypes') + media_types: deepAccess(req, 'mediaTypes'), }); } - let params = validBidRequests[0].params - let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com' + let params = validBidRequests[0].params; + let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com'; return { method: 'POST', url: url, data: JSON.stringify(payload), - bids: validBidRequests + bids: validBidRequests, }; }, - interpretResponse: function(serverResponse, bidRequest) { + interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; if (!serverResponse || !serverResponse.body) { - return bidResponses + return bidResponses; } let res = serverResponse.body; if (!res.bids || !res.bids.length) { - return [] + return []; } for (let x = 0; x < serverResponse.body.bids.length; x++) { - let bid = serverResponse.body.bids[x] + let bid = serverResponse.body.bids[x]; bidResponses.push({ requestId: bid.bid_id, @@ -152,47 +170,45 @@ export const spec = { netRevenue: true, currency: 'USD', meta: { - advertiserDomains: bid.adomain - } - }) + advertiserDomains: bid.adomain, + }, + }); } return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { - const syncs = [] + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + const syncs = []; if (!serverResponses.length || !serverResponses[0].body) { - return syncs + return syncs; } - let pixels = serverResponses[0].body.pixels + let pixels = serverResponses[0].body.pixels; if (!pixels || !pixels.length) { - return syncs + return syncs; } - let gdprParams = null + let gdprParams = ''; if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`; } else { - gdprParams = `gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; } } - for (let x = 0; x < pixels.length; x++) { - let pixel = pixels[x] - - if ((pixel.type === 'iframe' && syncOptions.iframeEnabled) || - (pixel.type === 'image' && syncOptions.pixelEnabled)) { - if (gdprParams && gdprParams.length) { - pixel = (pixel.indexOf('?') === -1 ? '?' : '&') + gdprParams - } - syncs.push(pixel) - } + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + return [ + { + type: 'iframe', + url: 'https://media.reset-digital.com/prebid/async_usersync.html?' + gdprParams.length ? gdprParams : '', + }, + ]; } - return syncs; - } + }, }; registerBidder(spec); diff --git a/modules/revcontentBidAdapter.js b/modules/revcontentBidAdapter.js index 5bf7dd691e7..f1d5521f780 100644 --- a/modules/revcontentBidAdapter.js +++ b/modules/revcontentBidAdapter.js @@ -3,9 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import {_map, deepAccess, getAdUnitSizes, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; +import {_map, deepAccess, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; import {parseDomain} from '../src/refererDetection.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'revcontent'; const NATIVE_PARAMS = { diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 1625912ddb8..36513aeda47 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isStr} from '../src/utils.js'; +import {deepAccess, isStr, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; @@ -183,6 +183,13 @@ export const spec = { } return syncs }, + + onTimeout: function (data) { + let url = raiGetTimeoutURL(data); + if (url) { + triggerPixel(url); + } + } }; registerBidder(spec); @@ -332,3 +339,15 @@ function raiGetFloor(bid, config) { return 0 } } + +function raiGetTimeoutURL(data) { + let {params, timeout} = data[0] + let url = 'https://s.richaudience.com/err/?ec=6&ev=[timeout_publisher]&pla=[placement_hash]&int=PREBID&pltfm=&node=&dm=[domain]'; + + url = url.replace('[timeout_publisher]', timeout) + url = url.replace('[placement_hash]', params[0].pid) + if (REFERER != null) { + url = url.replace('[domain]', document.location.host) + } + return url +} diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index d5c6469db12..78854129f8a 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -7,7 +19,8 @@ const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'rise'; const ADAPTER_VERSION = '6.0.0'; const TTL = 360; -const CURRENCY = 'USD'; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_GVLID = 1043; const DEFAULT_SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; const MODES = { PRODUCTION: 'hb-multi', @@ -20,7 +33,11 @@ const SUPPORTED_SYNC_METHODS = { export const spec = { code: BIDDER_CODE, - gvlid: 1043, + aliases: [ + { code: 'risexchange', gvlid: DEFAULT_GVLID }, + { code: 'openwebxchange', gvlid: 280 } + ], + gvlid: DEFAULT_GVLID, version: ADAPTER_VERSION, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { @@ -61,7 +78,7 @@ export const spec = { const bidResponse = { requestId: adUnit.requestId, cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, + currency: adUnit.currency || DEFAULT_CURRENCY, width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, @@ -128,18 +145,20 @@ registerBidder(spec); /** * Get floor price * @param bid {bid} + * @param mediaType {string} + * @param currency {string} * @returns {Number} */ -function getFloor(bid, mediaType) { +function getFloor(bid, mediaType, currency) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ - currency: CURRENCY, + currency: currency, mediaType: mediaType, size: '*' }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; } /** @@ -277,7 +296,7 @@ function generateBidParameters(bid, bidderRequest) { const {params} = bid; const mediaType = isBanner(bid) ? BANNER : VIDEO; const sizesArray = getSizesArray(bid, mediaType); - + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; // fix floor price in case of NAN if (isNaN(params.floorPrice)) { params.floorPrice = 0; @@ -287,12 +306,13 @@ function generateBidParameters(bid, bidderRequest) { mediaType, adUnitCode: getBidIdParameter('adUnitCode', bid), sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), bidId: getBidIdParameter('bidId', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), transactionId: bid.ortb2Imp?.ext?.tid, - coppa: 0 + coppa: 0, }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index f0837cb5508..ac0ea559c88 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -26,6 +26,7 @@ The adapter supports Video(instream). | `testMode` | optional | Boolean | This activates the test mode | false | `rtbDomain` | optional | String | Sets the seller end point | "www.test.com" | `is_wrapper` | private | Boolean | Please don't use unless your account manager asked you to | false +| `currency` | optional | String | 3 letters currency | "EUR" # Test Parameters diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 4ca4e4f90a9..5f94ec3dea4 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -195,7 +195,7 @@ registerBidder(spec); /** * @param {object} slot Ad Unit Params by Prebid - * @returns {int} floor by imp type + * @returns {number} floor by imp type */ function applyFloor(slot) { const floors = []; @@ -350,7 +350,7 @@ function mapNative(slot) { /** * @param {object} slot Slot config by Prebid - * @returns {array} Request Assets by OpenRTB Native Ads 1.1 §4.2 + * @returns {Array} Request Assets by OpenRTB Native Ads 1.1 §4.2 */ function mapNativeAssets(slot) { const params = slot.nativeParams || deepAccess(slot, 'mediaTypes.native'); @@ -413,7 +413,7 @@ function mapNativeAssets(slot) { /** * @param {object} image Prebid native.image/icon - * @param {int} type Image or icon code + * @param {number} type Image or icon code * @returns {object} Request Image by OpenRTB Native Ads 1.1 §4.4 */ function mapNativeImage(image, type) { diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 633c4f4cdc1..8968d4c795a 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -215,7 +215,8 @@ const setEventsListeners = (function () { [CONSTANTS.EVENTS.AUCTION_INIT]: ['onAuctionInitEvent'], [CONSTANTS.EVENTS.AUCTION_END]: ['onAuctionEndEvent', getAdUnitTargeting], [CONSTANTS.EVENTS.BID_RESPONSE]: ['onBidResponseEvent'], - [CONSTANTS.EVENTS.BID_REQUESTED]: ['onBidRequestEvent'] + [CONSTANTS.EVENTS.BID_REQUESTED]: ['onBidRequestEvent'], + [CONSTANTS.EVENTS.BID_ACCEPTED]: ['onBidAcceptedEvent'] }).forEach(([ev, [handler, preprocess]]) => { events.on(ev, (args) => { preprocess && preprocess(args); @@ -385,7 +386,7 @@ export function getAdUnitTargeting(auction) { /** * deep merge array of objects - * @param {array} arr - objects array + * @param {Array} arr - objects array * @return {Object} merged object */ export function deepMerge(arr) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index a8842facb6a..9882eb23ac3 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -7,7 +7,6 @@ import { find } from '../src/polyfill.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { Renderer } from '../src/Renderer.js'; import { - convertTypes, deepAccess, deepSetValue, formatQS, @@ -18,7 +17,9 @@ import { logMessage, logWarn, mergeDeep, - parseSizesInput, _each + parseSizesInput, + pick, + _each } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; @@ -124,6 +125,7 @@ var sizeMap = { 278: '320x500', 282: '320x400', 288: '640x380', + 484: '720x1280', 524: '1x2', 548: '500x1000', 550: '980x480', @@ -139,7 +141,9 @@ var sizeMap = { 576: '610x877', 578: '980x552', 580: '505x656', - 622: '192x160' + 622: '192x160', + 632: '1200x450', + 634: '340x450' }; _each(sizeMap, (item, key) => sizeMap[item] = key); @@ -196,8 +200,8 @@ export const converter = ortbConverter({ if (config.getConfig('s2sConfig.defaultTtl')) { imp.exp = config.getConfig('s2sConfig.defaultTtl'); }; - bidRequest.params.position === 'atf' && (imp.video.pos = 1); - bidRequest.params.position === 'btf' && (imp.video.pos = 3); + bidRequest.params.position === 'atf' && imp.video && (imp.video.pos = 1); + bidRequest.params.position === 'btf' && imp.video && (imp.video.pos = 3); delete imp.ext?.prebid?.storedrequest; if (bidRequest.params.bidonmultiformat === true && bidRequestType.length > 1) { @@ -405,6 +409,8 @@ export const spec = { 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', + 'o_ae', + 'o_cdep', 'rp_floor', 'rp_secure', 'tk_user_key' @@ -478,6 +484,7 @@ export const spec = { 'x_source.tid': bidderRequest.ortb2?.source?.tid, 'x_imp.ext.tid': bidRequest.ortb2Imp?.ext?.tid, 'l_pb_bid_id': bidRequest.bidId, + 'o_cdep': bidRequest.ortb2?.device?.ext?.cdep, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), @@ -517,6 +524,10 @@ export const spec = { if (configUserId) { data['ppuid'] = configUserId; } + + if (bidRequest?.ortb2Imp?.ext?.ae) { + data['o_ae'] = 1; + } // loop through userIds and add to request if (bidRequest.userIdAsEids) { bidRequest.userIdAsEids.forEach(eid => { @@ -537,7 +548,9 @@ export const spec = { data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; } else { // add anything else with this generic format - data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; + // if rubicon drop ^ + const id = eid.source === 'rubiconproject.com' ? eid.uids[0].id : `${eid.uids[0].id}^${eid.uids[0].atype || ''}` + data[`eid_${eid.source}`] = id; } // send AE "ppuid" signal if exists, and hasn't already been sent if (!data['ppuid']) { @@ -614,7 +627,7 @@ export const spec = { * @param {*} responseObj * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object - * @return {Bid[]} An array of bids which + * @return {{fledgeAuctionConfigs: *, bids: *}} An array of bids which */ interpretResponse: function (responseObj, request) { responseObj = responseObj.body; @@ -624,7 +637,6 @@ export const spec = { if (!responseObj || typeof responseObj !== 'object') { return []; } - // Response from PBS Java openRTB if (responseObj.seatbid) { const responseErrors = deepAccess(responseObj, 'ext.errors.rubicon'); @@ -650,7 +662,7 @@ export const spec = { return []; } - return ads.reduce((bids, ad, i) => { + let bids = ads.reduce((bids, ad, i) => { (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; if (ad.status !== 'ok') { @@ -711,6 +723,16 @@ export const spec = { }, []).sort((adA, adB) => { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); + + let fledgeAuctionConfigs = responseObj.component_auction_config?.map(config => { + return { config, bidId: config.bidId } + }); + + if (fledgeAuctionConfigs) { + return { bids, fledgeAuctionConfigs }; + } else { + return bids; + } }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (!hasSynced && syncOptions.iframeEnabled) { @@ -743,19 +765,6 @@ export const spec = { url: `https://${rubiConf.syncHost || 'eus'}.rubiconproject.com/usync.html` + params }; } - }, - /** - * Covert bid param types for S2S - * @param {Object} params bid params - * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol - * @return {Object} params bid params - */ - transformBidParams: function(params, isOpenRtb) { - return convertTypes({ - 'accountId': 'number', - 'siteId': 'number', - 'zoneId': 'number' - }, params); } }; @@ -952,6 +961,33 @@ function applyFPD(bidRequest, mediaType, data) { if (data['tg_i.pbadslot']) { delete data['tg_i.dfp_ad_unit_code']; } + + // High Entropy stuff -> sua object is the ORTB standard (default to pass unless specifically disabled) + const clientHints = deepAccess(fpd, 'device.sua'); + if (clientHints && rubiConf.chEnabled !== false) { + // pick out client hints we want to send (any that are undefined or empty will NOT be sent) + pick(clientHints, [ + 'architecture', arch => data.m_ch_arch = arch, + 'bitness', bitness => data.m_ch_bitness = bitness, + 'browsers', browsers => { + if (!Array.isArray(browsers)) return; + // reduce down into ua and full version list attributes + const [ua, fullVer] = browsers.reduce((accum, browserData) => { + accum[0].push(`"${browserData?.brand}"|v="${browserData?.version?.[0]}"`); + accum[1].push(`"${browserData?.brand}"|v="${browserData?.version?.join?.('.')}"`); + return accum; + }, [[], []]); + data.m_ch_ua = ua?.join?.(','); + data.m_ch_full_ver = fullVer?.join?.(','); + }, + 'mobile', isMobile => data.m_ch_mobile = `?${isMobile}`, + 'model', model => data.m_ch_model = model, + 'platform', platform => { + data.m_ch_platform = platform?.brand; + data.m_ch_platform_ver = platform?.version?.join?.('.'); + } + ]) + } } else { if (Object.keys(impExt).length) { mergeDeep(data.imp[0].ext, impExt); @@ -1133,8 +1169,7 @@ export function hasValidVideoParams(bid) { var requiredParams = { mimes: arrayType, protocols: arrayType, - linearity: numberType, - api: arrayType + linearity: numberType } // loop through each param and verify it has the correct Object.keys(requiredParams).forEach(function(param) { @@ -1149,7 +1184,7 @@ export function hasValidVideoParams(bid) { /** * Make sure the required params are present * @param {Object} schain - * @param {Bool} + * @param {boolean} */ export function hasValidSupplyChainParams(schain) { let isValid = false; diff --git a/modules/schain.js b/modules/schain.js index 2991bb5b3d5..726679b133f 100644 --- a/modules/schain.js +++ b/modules/schain.js @@ -1,16 +1,17 @@ -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import adapterManager from '../src/adapterManager.js'; import { - isNumber, - isStr, + _each, + deepAccess, + deepClone, + deepSetValue, isArray, + isInteger, + isNumber, isPlainObject, - hasOwn, + isStr, logError, - isInteger, - _each, - logWarn, - deepAccess, deepSetValue, deepClone + logWarn } from '../src/utils.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; @@ -63,7 +64,7 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // ext: Object [optional] - if (hasOwn(schainObject, 'ext')) { + if (schainObject.hasOwnProperty('ext')) { if (!isPlainObject(schainObject.ext)) { appendFailMsg(`schain.config.ext` + shouldBeAnObject); } @@ -92,28 +93,28 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // rid: String [Optional] - if (hasOwn(node, 'rid')) { + if (node.hasOwnProperty('rid')) { if (!isStr(node.rid)) { appendFailMsg(`schain.config.nodes[${index}].rid` + shouldBeAString); } } // name: String [Optional] - if (hasOwn(node, 'name')) { + if (node.hasOwnProperty('name')) { if (!isStr(node.name)) { appendFailMsg(`schain.config.nodes[${index}].name` + shouldBeAString); } } // domain: String [Optional] - if (hasOwn(node, 'domain')) { + if (node.hasOwnProperty('domain')) { if (!isStr(node.domain)) { appendFailMsg(`schain.config.nodes[${index}].domain` + shouldBeAString); } } // ext: Object [Optional] - if (hasOwn(node, 'ext')) { + if (node.hasOwnProperty('ext')) { if (!isPlainObject(node.ext)) { appendFailMsg(`schain.config.nodes[${index}].ext` + shouldBeAnObject); } diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index 7ac7d048c50..51c326c2954 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -107,7 +107,6 @@ function buildBidRequest(validBidRequest) { return mediaTypesMap[pbjsType]; } ); - const bidRequest = { id: validBidRequest.bidId, transactionId: validBidRequest.ortb2Imp?.ext?.tid, @@ -115,6 +114,7 @@ function buildBidRequest(validBidRequest) { supplyTypes: mediaTypes, adUnitId: params.adUnitId, adUnitCode: validBidRequest.adUnitCode, + geom: geom(validBidRequest.adUnitCode), placement: params.placement, requestCount: validBidRequest.bidderRequestsCount || 1, // FIXME : in unit test the parameter bidderRequestsCount is undefined }; @@ -198,6 +198,27 @@ function ttfb() { return ttfb >= 0 && ttfb <= performance.now() ? ttfb : 0; } +function geom(adunitCode) { + const slot = document.getElementById(adunitCode); + if (slot) { + const scrollY = window.scrollY; + const { top, left, width, height } = slot.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight, + }; + + return { + scrollY, + top, + left, + width, + height, + viewport, + }; + } +} + export function getTimeoutUrl(data) { let queryParams = ''; if ( diff --git a/modules/sharethroughAnalyticsAdapter.js b/modules/sharethroughAnalyticsAdapter.js index 6502c7e3a53..dc621e8da92 100644 --- a/modules/sharethroughAnalyticsAdapter.js +++ b/modules/sharethroughAnalyticsAdapter.js @@ -1,6 +1,6 @@ -import { tryAppendQueryString } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index a8beb018b73..53cb67c4e6d 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -45,6 +45,7 @@ export const sharethroughAdapterSpec = { dnt: navigator.doNotTrack === '1' ? 1 : 0, h: window.screen.height, w: window.screen.width, + ext: {}, }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, @@ -63,6 +64,10 @@ export const sharethroughAdapterSpec = { test: 0, }; + if (bidderRequest.ortb2?.device?.ext?.cdep) { + req.device.ext['cdep'] = bidderRequest.ortb2.device.ext.cdep; + } + req.user = nullish(firstPartyData.user, {}); if (!req.user.ext) req.user.ext = {}; req.user.ext.eids = bidRequests[0].userIdAsEids || []; @@ -216,26 +221,11 @@ export const sharethroughAdapterSpec = { }); }, - getUserSyncs: (syncOptions, serverResponses, gdprConsent, gppConsent) => { + getUserSyncs: (syncOptions, serverResponses) => { const shouldCookieSync = syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; - let syncurl = ''; - - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - if (gppConsent) { - syncurl += '&gpp=' + encodeURIComponent(gppConsent?.gppString); - syncurl += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); - } - - return shouldCookieSync ? serverResponses[0].body.cookieSyncUrls.map((url) => ( - { type: 'image', - url: url + syncurl - })) : []; + return shouldCookieSync ? serverResponses[0].body.cookieSyncUrls.map((url) => ({ type: 'image', url: url })) : []; }, // Empty implementation for prebid core to be able to find it diff --git a/modules/shinezBidAdapter.js b/modules/shinezBidAdapter.js index 96b6d281fdc..47fca317de2 100644 --- a/modules/shinezBidAdapter.js +++ b/modules/shinezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; diff --git a/modules/shinezRtbBidAdapter.js b/modules/shinezRtbBidAdapter.js new file mode 100644 index 00000000000..d1d9f36a569 --- /dev/null +++ b/modules/shinezRtbBidAdapter.js @@ -0,0 +1,336 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'shinezRtb'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.sweetgum.io`; +} + +export function extractCID(params) { + return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; +} + +export function extractPID(params) { + return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; +} + +export function extractSubDomain(params) { + return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.sweetgum.io/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.sweetgum.io/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/shinezRtbBidAdapter.md b/modules/shinezRtbBidAdapter.md new file mode 100644 index 00000000000..e9190c2a9c4 --- /dev/null +++ b/modules/shinezRtbBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Shinez RTB Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** tech-team@shinez.io + +# Description + +Module that connects to Shinez RTB demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'shinezRtb', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index a241cb71a5d..bd2706a21d5 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -1,10 +1,9 @@ import { deepAccess, - getBidIdParameter, getWindowTop, triggerPixel, logInfo, - logError + logError, getBidIdParameter } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; @@ -29,8 +28,11 @@ function getEnvURLs(isStage) { } } +const GVLID = 111; + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['showheroesBs'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function(bid) { diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index d212d98f50b..5ddb2e410cb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -63,7 +63,7 @@ export function isUsingNewSizeMapping(adUnits) { does not recognize. @params {Array} adUnits @returns {Array} validateAdUnits - Unrecognized properties are deleted. -*/ + */ export function checkAdUnitSetupHook(adUnits) { const validateSizeConfig = function (mediaType, sizeConfig, adUnitCode) { let isValid = true; diff --git a/modules/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index 447e314958f..9cdb85d4f25 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, parseSizesInput, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue, parseSizesInput} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -13,11 +13,11 @@ export const spec = { aliases: [{ code: 'scm', gvlid: 102 }], supportedMediaTypes: ['video', 'banner'], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { let isValid = false; if (typeof bid.params !== 'undefined' && !isNaN(parseInt(getValue(bid.params, 'placementId'))) && parseInt(getValue(bid.params, 'placementId')) > 0) { @@ -26,11 +26,11 @@ export const spec = { return isValid; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const bids = validBidRequests.map(buildRequestObject); const payload = { @@ -55,11 +55,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, request) { const bidResponses = []; serverResponse = serverResponse.body; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 1b50e033074..b735953d099 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -1,22 +1,12 @@ -import { - chunk, - deepAccess, - deepSetValue, - fill, - getAdUnitSizes, - getDNT, - getMaxValueFromArray, - getMinValueFromArray, - isEmpty, - isNumber, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, getDNT, isEmpty, isNumber, logError, logInfo} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import CONSTANTS from '../src/constants.json'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {fill} from '../libraries/appnexusUtils/anUtils.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const { NATIVE_IMAGE_TYPES } = CONSTANTS; const BIDDER_CODE = 'smaato'; @@ -466,7 +456,7 @@ function createAdPodImp(bidRequest, videoMediaType) { }); } else { // all maxdurations should be the same - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); imps.map((imp, index) => { const sequence = index + 1; imp.video.maxduration = maxDuration @@ -481,7 +471,7 @@ function createAdPodImp(bidRequest, videoMediaType) { function getAdPodNumberOfPlacements(videoMediaType) { const {adPodDurationSec, durationRangeSec, requireExactDuration} = videoMediaType - const minAllowedDuration = getMinValueFromArray(durationRangeSec) + const minAllowedDuration = Math.min(...durationRangeSec) const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration) return requireExactDuration diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index ca43c26ffd7..313d466bd2e 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -1,4 +1,4 @@ -import { deepAccess, deepClone, logError, isFn, isPlainObject } from '../src/utils.js'; +import { deepAccess, deepClone, isArrayOfNums, isFn, isInteger, isPlainObject, logError } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -55,20 +55,53 @@ export const spec = { * Fills the payload with specific video attributes. * * @param {*} payload Payload that will be sent in the ServerRequest - * @param {*} videoMediaType Video media type. + * @param {*} videoMediaType Video media type */ fillPayloadForVideoBidRequest: function(payload, videoMediaType, videoParams) { const playerSize = videoMediaType.playerSize[0]; - payload.isVideo = videoMediaType.context === 'instream'; + const map = { + maxbitrate: 'vbrmax', + maxduration: 'vdmax', + minbitrate: 'vbrmin', + minduration: 'vdmin', + placement: 'vpt', + plcmt: 'vplcmt', + skip: 'skip' + }; + payload.mediaType = VIDEO; + payload.isVideo = videoMediaType.context === 'instream'; + payload.videoData = {}; + + for (const [key, value] of Object.entries(map)) { + payload.videoData = { + ...payload.videoData, + ...this.getValuableProperty(value, videoMediaType[key]) + }; + } + payload.videoData = { - videoProtocol: this.getProtocolForVideoBidRequest(videoMediaType, videoParams), - playerWidth: playerSize[0], - playerHeight: playerSize[1], - adBreak: this.getStartDelayForVideoBidRequest(videoMediaType, videoParams) + ...payload.videoData, + ...this.getValuableProperty('playerWidth', playerSize[0]), + ...this.getValuableProperty('playerHeight', playerSize[1]), + ...this.getValuableProperty('adBreak', this.getStartDelayForVideoBidRequest(videoMediaType, videoParams)), + ...this.getValuableProperty('videoProtocol', this.getProtocolForVideoBidRequest(videoMediaType, videoParams)), + ...(isArrayOfNums(videoMediaType.api) && videoMediaType.api.length ? { iabframeworks: videoMediaType.api.toString() } : {}), + ...(isArrayOfNums(videoMediaType.playbackmethod) && videoMediaType.playbackmethod.length ? { vpmt: videoMediaType.playbackmethod } : {}) }; }, + /** + * Gets a property object if the value not falsy + * @param {string} property + * @param {number} value + * @returns object with the property or empty + */ + getValuableProperty: function(property, value) { + return typeof property === 'string' && isInteger(value) && value + ? { [property]: value } : {}; + }, + /** * Gets the protocols from either videoParams or VideoMediaType * @param {*} videoMediaType diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index d91b62729bc..45cc45192ef 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -1,4 +1,17 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, generateUUID, isEmpty, _each, logMessage, logWarn, isFn, isPlainObject } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + generateUUID, + isEmpty, + _each, + logMessage, + logWarn, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md index 853f06d6baf..50f78660458 100644 --- a/modules/smartxBidAdapter.md +++ b/modules/smartxBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: smartclip Bidder Adapter Module Type: Bidder Adapter -Maintainer: adtech@smartclip.tv +Maintainer: bidding@smartclip.tv ``` # Description @@ -170,4 +170,4 @@ This adapter requires setup and approval from the smartclip team. } }], }]; -``` \ No newline at end of file +``` diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 1644a15e92d..2409bebbc59 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -6,7 +6,13 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'smartyads'; -const AD_URL = 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'; +const GVLID = 534; +const adUrls = { + US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + SGP: 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' +} + const URL_SYNC = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a'; function isBidResponseValid(bid) { @@ -26,8 +32,28 @@ function isBidResponseValid(bid) { } } +function getAdUrlByRegion(bid) { + let adUrl; + + if (bid.params.region && adUrls[bid.params.region]) { + adUrl = adUrls[bid.params.region]; + } else { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + if (region === 'Europe') adUrl = adUrls['EU']; + else adUrl = adUrls['US_EAST']; + } catch (err) { + adUrl = adUrls['US_EAST']; + } + } + + return adUrl; +} + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -73,8 +99,11 @@ export const spec = { } const len = validBidRequests.length; + let adUrl; + for (let i = 0; i < len; i++) { let bid = validBidRequests[i]; + if (i === 0) adUrl = getAdUrlByRegion(bid); let traff = bid.params.traffic || BANNER placements.push({ placementId: bid.params.sourceid, @@ -87,11 +116,12 @@ export const spec = { placements.schain = bid.schain; } } + return { method: 'POST', - url: AD_URL, + url: adUrl, data: request - }; + } }, interpretResponse: (serverResponse) => { @@ -129,16 +159,22 @@ export const spec = { if (bid.winUrl) { ajax(bid.winUrl, () => {}, JSON.stringify(bid)); } else { - ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); + } } }, onTimeout: function(bid) { - ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); + } }, onBidderError: function(bid) { - ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); + } }, }; diff --git a/modules/smartyadsBidAdapter.md b/modules/smartyadsBidAdapter.md index e0d6023a794..443d5ab5978 100644 --- a/modules/smartyadsBidAdapter.md +++ b/modules/smartyadsBidAdapter.md @@ -14,10 +14,11 @@ Module that connects to SmartyAds' demand sources | Name | Scope | Description | Example | | :------------ | :------- | :------------------------ | :------------------- | -| `sourceid` | required (for prebid.js) | placement ID | "0" | -| `host` | required (for prebid-server) | const value, set to "prebid" | "prebid" | -| `accountid` | required (for prebid-server) | partner ID | "1901" | +| `sourceid` | required (for prebid.js) | Placement ID | "0" | +| `host` | required (for prebid-server) | Const value, set to "prebid" | "prebid" | +| `accountid` | required (for prebid-server) | Partner ID | "1901" | | `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | +| `region` | optional (for prebid.js) | Prefix of the region to which prebid must send requests. Possible values: "US_EAST", "EU" | "US_EAST" | # Test Parameters ``` @@ -35,7 +36,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'native' + traffic: 'native', + region: 'US_EAST' + } } ] @@ -55,7 +58,8 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'banner' + traffic: 'banner', + region: 'US_EAST' } } ] @@ -76,7 +80,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'video' + traffic: 'video', + region: 'US_EAST' + } } ] diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index b7a25ba58df..09c39e52825 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -4,9 +4,12 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +const GVL_ID = 639; + export const spec = { code: 'smilewanted', aliases: ['smile', 'sw'], + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. @@ -37,11 +40,13 @@ export const spec = { transactionId: bid.ortb2Imp?.ext?.tid, timeout: bidderRequest?.timeout, bidId: bid.bidId, - /** positionType is undocumented + /** + positionType is undocumented It is unclear what this parameter means. If it means the same as pos in openRTB, It should read from openRTB object - or from mediaTypes.banner.pos */ + or from mediaTypes.banner.pos + */ positionType: bid.params.positionType || '', prebidVersion: '$prebid.version$' }; diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index 489d0bcdc9e..6d32d8f97a2 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -105,7 +105,7 @@ registerBidder(spec); function getPage(bidderRequest) { return ( - getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || window.location.href + getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.page') || window.location.href ); } diff --git a/modules/sonobiAnalyticsAdapter.js b/modules/sonobiAnalyticsAdapter.js index 0057944b201..04a855b5be6 100644 --- a/modules/sonobiAnalyticsAdapter.js +++ b/modules/sonobiAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {ajaxBuilder} from '../src/ajax.js'; let ajax = ajaxBuilder(0); -const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; +export const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; const analyticsType = 'endpoint'; const QUEUE_TIMEOUT_DEFAULT = 200; const { diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 704275cc1bf..2c84854e507 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -1,11 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; +import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { userSync } from '../src/userSync.js'; import { bidderSettings } from '../src/bidderSettings.js'; import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'sonobi'; const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; const PAGEVIEW_ID = generateUUID(); @@ -60,23 +61,15 @@ export const spec = { */ buildRequests: (validBidRequests, bidderRequest) => { const bids = validBidRequests.map(bid => { - let mediaType; - - if (deepAccess(bid, 'mediaTypes.video')) { - mediaType = 'video'; - } else if (deepAccess(bid, 'mediaTypes.banner')) { - mediaType = 'display'; - } - let slotIdentifier = _validateSlot(bid); if (/^[\/]?[\d]+[[\/].+[\/]?]?$/.test(slotIdentifier)) { slotIdentifier = slotIdentifier.charAt(0) === '/' ? slotIdentifier : '/' + slotIdentifier; return { - [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else if (/^[0-9a-fA-F]{20}$/.test(slotIdentifier) && slotIdentifier.length === 20) { return { - [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else { logError(`The ad unit code or Sonobi Placement id for slot ${bid.bidId} is invalid`); @@ -157,6 +150,11 @@ export const spec = { payload.coppa = 0; } + if (deepAccess(bidderRequest, 'ortb2.experianRtidData') && deepAccess(bidderRequest, 'ortb2.experianRtidKey')) { + payload.expData = deepAccess(bidderRequest, 'ortb2.experianRtidData'); + payload.expKey = deepAccess(bidderRequest, 'ortb2.experianRtidKey'); + } + // If there is no key_maker data, then don't make the request. if (isEmpty(data)) { return null; @@ -336,10 +334,28 @@ function _validateGPID(bid) { return '' } -function _validateMediaType(mediaType) { +function _validateMediaType(bidRequest) { + let mediaType; + if (deepAccess(bidRequest, 'mediaTypes.video')) { + mediaType = 'video'; + } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { + mediaType = 'display'; + } + let mediaTypeValidation = ''; if (mediaType === 'video') { mediaTypeValidation = 'c=v,'; + if (deepAccess(bidRequest, 'mediaTypes.video.playbackmethod')) { + mediaTypeValidation = `${mediaTypeValidation}pm=${deepAccess(bidRequest, 'mediaTypes.video.playbackmethod').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.placement')) { + let placement = deepAccess(bidRequest, 'mediaTypes.video.placement'); + mediaTypeValidation = `${mediaTypeValidation}p=${placement},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.plcmt')) { + let plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt'); + mediaTypeValidation = `${mediaTypeValidation}pl=${plcmt},`; + } } else if (mediaType === 'display') { mediaTypeValidation = 'c=d,'; } diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 0d077ad2ae3..0ff5d842135 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,13 +1,12 @@ import { _each, - getBidIdParameter, isArray, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, - logWarn + logWarn, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { @@ -45,7 +44,6 @@ const ORTB_VIDEO_PARAMS = { const REQUIRED_VIDEO_PARAMS = { context: (value) => value !== ADPOD, mimes: ORTB_VIDEO_PARAMS.mimes, - minduration: ORTB_VIDEO_PARAMS.minduration, maxduration: ORTB_VIDEO_PARAMS.maxduration, protocols: ORTB_VIDEO_PARAMS.protocols } @@ -174,6 +172,11 @@ export const spec = { deepSetValue(sovrnBidReq, 'source.tid', tid) } + const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa'); + if (coppa) { + deepSetValue(sovrnBidReq, 'regs.coppa', 1); + } + if (bidderRequest.gdprConsent) { deepSetValue(sovrnBidReq, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); deepSetValue(sovrnBidReq, 'user.ext.consent', bidderRequest.gdprConsent.consentString) @@ -211,7 +214,7 @@ export const spec = { * Format Sovrn responses as Prebid bid responses * @param {id, seatbid} sovrnResponse A successful response from Sovrn. * @return {Bid[]} An array of formatted bids. - */ + */ interpretResponse: function({ body: {id, seatbid} }) { if (!id || !seatbid || !Array.isArray(seatbid)) return [] diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js new file mode 100644 index 00000000000..bfb527d46f2 --- /dev/null +++ b/modules/sparteoBidAdapter.js @@ -0,0 +1,166 @@ +import { deepAccess, deepSetValue, logError, parseSizesInput, triggerPixel } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'sparteo'; +const GVLID = 1028; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; +let isSynced = window.sparteoCrossfire?.started || false; + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: TTL // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + if (bidderRequest.bids[0].params.networkId) { + deepSetValue(request, 'site.publisher.ext.params.networkId', bidderRequest.bids[0].params.networkId); + } + + if (bidderRequest.bids[0].params.publisherId) { + deepSetValue(request, 'site.publisher.ext.params.publisherId', bidderRequest.bids[0].params.publisherId); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + deepSetValue(imp, 'ext.sparteo.params', bidRequest.params); + + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + context.mediaType = deepAccess(bid, 'ext.prebid.type'); + + const response = buildBidResponse(bid, context); + + if (context.mediaType == 'video') { + response.nurl = bid.nurl; + response.vastUrl = deepAccess(bid, 'ext.prebid.cache.vastXml.url') ?? null; + } + + return response; + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + let bannerParams = deepAccess(bid, 'mediaTypes.banner'); + let videoParams = deepAccess(bid, 'mediaTypes.video'); + + if (!bid.params) { + logError('The bid params are missing'); + return false; + } + + if (!bid.params.networkId && !bid.params.publisherId) { + logError('The networkId or publisherId is required'); + return false; + } + + if (!bannerParams && !videoParams) { + logError('The placement must be of banner or video type'); + return false; + } + + /** + * BANNER checks + */ + + if (bannerParams) { + let sizes = bannerParams.sizes; + + if (!sizes || parseSizesInput(sizes).length == 0) { + logError('mediaTypes.banner.sizes must be set for banner placement at the right format.'); + return false; + } + } + + /** + * VIDEO checks + */ + + if (videoParams) { + if (parseSizesInput(videoParams.playerSize).length == 0) { + logError('mediaTypes.video.playerSize must be set for video placement at the right format.'); + return false; + } + } + + return true; + }, + + buildRequests: function (bidRequests, bidderRequest) { + const payload = converter.toORTB({bidRequests, bidderRequest}) + + return { + method: HTTP_METHOD, + url: bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL, + data: payload + }; + }, + + interpretResponse: function (serverResponse, requests) { + const bids = converter.fromORTB({response: serverResponse.body, request: requests.data}).bids; + + return bids; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncurl = ''; + + if (!isSynced && !window.sparteoCrossfire?.started) { + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&usp_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + isSynced = true; + + window.sparteoCrossfire = { + started: true + }; + + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } + } + }, + + onTimeout: function (timeoutData) {}, + + onBidWon: function (bid) { + if (bid && bid.nurl) { + triggerPixel(bid.nurl, null); + } + }, + + onSetTargeting: function (bid) {} +}; + +registerBidder(spec); diff --git a/modules/sparteoBidAdapter.md b/modules/sparteoBidAdapter.md new file mode 100644 index 00000000000..774d9211d9d --- /dev/null +++ b/modules/sparteoBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Sparteo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@sparteo.com +``` + +# Description + +Module that connects to Sparteo's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + }, + bids: [ + { + bidder: 'sparteo', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 874207adcf8..017544cc596 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -1,4 +1,20 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, deepSetValue, isEmpty, _each, logMessage, logWarn, isBoolean, isNumber, isPlainObject, isFn, setScriptAttributes } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + deepSetValue, + isEmpty, + _each, + logMessage, + logWarn, + isBoolean, + isNumber, + isPlainObject, + isFn, + setScriptAttributes, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; diff --git a/modules/ssmasBidAdapter.js b/modules/ssmasBidAdapter.js index 3005910789b..0b70a80e757 100644 --- a/modules/ssmasBidAdapter.js +++ b/modules/ssmasBidAdapter.js @@ -113,12 +113,19 @@ export const spec = { params.push(`ccpa_consent=${uspConsent.consentString}`); } - if (syncOptions.pixelEnabled && serverResponses.length > 0) { + if (syncOptions.iframeEnabled && serverResponses.length > 0) { syncs.push({ - type: 'image', - url: `${SYNC_URL}?${params.join('&')}` + type: 'iframe', + url: `${SYNC_URL}/iframe?${params.join('&')}` }); } + + // if (syncOptions.pixelEnabled && serverResponses.length > 0) { + // syncs.push({ + // type: 'image', + // url: `${SYNC_URL}/image?${params.join('&')}` + // }); + // } return syncs; }, }; diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 70db18c61e1..c351b76d7ea 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.9'; +const BIDDER_VERSION = '5.92'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; @@ -38,7 +38,7 @@ var nativeAssetMap = { /** * return native asset type, based on asset id - * @param {int} id - native asset id + * @param {number} id - native asset id * @returns {string} asset type */ const getNativeAssetType = id => { @@ -164,7 +164,7 @@ const applyClientHints = ortbRequest => { Check / generate page view id Should be generated dureing first call to applyClientHints(), and re-generated if pathname has changed - */ + */ if (!pageView.id || location.pathname !== pageView.path) { pageView.path = location.pathname; pageView.id = Math.floor(1E20 * Math.random()).toString(); @@ -199,6 +199,22 @@ const applyClientHints = ortbRequest => { ortbRequest.user = { ...ortbRequest.user, ...ch }; }; +const applyTopics = (validBidRequest, ortbRequest) => { + const userData = validBidRequest.ortb2?.user?.data || []; + const topicsData = userData.filter(dataObj => { + const segtax = dataObj.ext?.segtax; + return segtax >= 600 && segtax <= 609; + })[0]; + + // format topics obj for exchange + if (topicsData) { + topicsData.id = `${topicsData.ext.segtax}`; + topicsData.name = 'topics'; + delete (topicsData.ext); + ortbRequest.user.data.push(topicsData); + } +}; + const applyUserIds = (validBidRequest, ortbRequest) => { const eids = validBidRequest.userIdAsEids if (eids && eids.length) { @@ -490,7 +506,7 @@ const mapImpression = slot => { } const isVideoAd = bid => { - const xmlTester = new RegExp(/^<\?xml/); + const xmlTester = new RegExp(/^<\?xml| { const [bidRequest] = validBidRequests; - const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; + const data = converter.toORTB({bidderRequest: bidderRequest, bidRequests: validBidRequests}); const {publisherId} = bidRequest.params; - const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); - const device = {ua: navigator.userAgent}; - const imps = getImps(validBidRequests); - const user = { - buyeruid: userData.getUserId(gdprConsent, uspConsent), - ext: {} - }; - const regs = { - coppa: 0, - ext: {} - }; - - if (gdprConsent.gdprApplies) { - user.ext.consent = bidderRequest.gdprConsent.consentString; - regs.ext.gdpr = 1; - } - - if (uspConsent) { - regs.ext.us_privacy = uspConsent; - } - - if (bidderRequest.ortb2?.regs?.gpp) { - regs.ext.gpp = bidderRequest.ortb2.regs.gpp; - regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (config.getConfig('coppa')) { - regs.coppa = 1; - } - - const ortb2 = bidderRequest.ortb2 || { - bcat: [], - badv: [], - wlang: [] - }; - - const request = { - id: bidderRequest.bidderRequestId, - imp: imps, - site, - device, - source: {fd: 1}, - tmax: bidderRequest.timeout, - bcat: ortb2.bcat || bidRequest.params.bcat || [], - badv: ortb2.badv || bidRequest.params.badv || [], - wlang: ortb2.wlang || bidRequest.params.wlang || [], - user, - regs, - ext: { - pageType: ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType - } - }; - - const url = [END_POINT_URL, publisherId].join('/'); + const url = END_POINT_URL + '?publisher=' + publisherId; return { url, method: 'POST', - data: JSON.stringify(request), + data: data, bids: validBidRequests, options: { withCredentials: false }, }; }, - interpretResponse: (serverResponse, {bids}) => { - if (!bids) { + interpretResponse: (serverResponse, request) => { + if (!request || !request.bids || !request.data) { return []; } - const {bidResponses, cur: currency} = getBidResponses(serverResponse); + if (!serverResponse || !serverResponse.body) { + return []; + } - if (!bidResponses) { + if (!serverResponse.body.seatbid || !serverResponse.body.seatbid.length || !serverResponse.body.seatbid[0].bid || !serverResponse.body.seatbid[0].bid.length) { return []; } - return bidResponses.map((bidResponse) => getBid(bids, currency, bidResponse)).filter(Boolean); + const bids = converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + return bids; }, onBidWon: (bid) => { if (bid.nurl) { @@ -182,6 +169,13 @@ export const spec = { queryParams.push('gpp=' + encodeURIComponent(gppConsent)); } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_IFRAME_URL + (queryParams.length ? '?' + queryParams.join('&') : '') + }); + } + if (syncOptions.pixelEnabled) { syncs.push({ type: 'image', @@ -190,6 +184,13 @@ export const spec = { } return syncs; }, + onTimeout: (timeoutData) => { + ajax(EVENT_ENDPOINT + '/timeout', null, JSON.stringify(timeoutData), {method: 'POST'}); + }, + + onBidderError: ({ error, bidderRequest }) => { + ajax(EVENT_ENDPOINT + '/bidError', null, JSON.stringify(error, bidderRequest), {method: 'POST'}); + }, }; function getSiteProperties({publisherId}, refererInfo, ortb2) { @@ -209,34 +210,76 @@ function getSiteProperties({publisherId}, refererInfo, ortb2) { } } -function getImps(validBidRequests) { - return validBidRequests.map((bid, id) => { - const {tagId, position} = bid.params; - const imp = { - id: id + 1, - banner: getBanners(bid, position), - tagid: tagId - } - if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({ - currency: CURRENCY, - mediaType: BANNER, - size: '*' - }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { - imp.bidfloor = parseFloat(floorInfo.floor); - imp.bidfloorcur = CURRENCY; - } - } else { - const {bidfloor = null, bidfloorcur = CURRENCY} = bid.params; - imp.bidfloor = bidfloor; - imp.bidfloorcur = bidfloorcur; - } - imp['ext'] = { - gpid: deepAccess(bid, 'ortb2Imp.ext.gpid') +function fillTaboolaReqData(bidderRequest, bidRequest, data) { + const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; + const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); + const device = {ua: navigator.userAgent}; + const user = { + buyeruid: userData.getUserId(gdprConsent, uspConsent), + ext: {} + }; + const regs = { + coppa: 0, + ext: {} + }; + + if (gdprConsent.gdprApplies) { + user.ext.consent = bidderRequest.gdprConsent.consentString; + regs.ext.gdpr = 1; + } + + if (uspConsent) { + regs.ext.us_privacy = uspConsent; + } + + if (bidderRequest.ortb2?.regs?.gpp) { + regs.ext.gpp = bidderRequest.ortb2.regs.gpp; + regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + + if (config.getConfig('coppa')) { + regs.coppa = 1; + } + + const ortb2 = bidderRequest.ortb2 || { + bcat: [], + badv: [], + wlang: [] + }; + + data.id = bidderRequest.bidderRequestId; + data.site = site; + data.device = device; + data.source = {fd: 1}; + data.tmax = (bidderRequest.timeout == undefined) ? undefined : parseInt(bidderRequest.timeout); + data.bcat = ortb2.bcat || bidRequest.params.bcat || []; + data.badv = ortb2.badv || bidRequest.params.badv || []; + data.wlang = ortb2.wlang || bidRequest.params.wlang || []; + data.user = user; + data.regs = regs; + deepSetValue(data, 'ext.pageType', ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType); +} + +function fillTaboolaImpData(bid, imp) { + const {tagId, position} = bid.params; + imp.banner = getBanners(bid, position); + imp.tagid = tagId; + + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: CURRENCY, + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + imp.bidfloor = parseFloat(floorInfo.floor); + imp.bidfloorcur = CURRENCY; } - return imp; - }); + } else { + const {bidfloor = null, bidfloorcur = CURRENCY} = bid.params; + imp.bidfloor = bidfloor; + imp.bidfloorcur = bidfloorcur; + } + deepSetValue(imp, 'ext.gpid', deepAccess(bid, 'ortb2Imp.ext.gpid')); } function getBanners(bid, pos) { @@ -257,51 +300,4 @@ function getSizes(sizes) { } } -function getBidResponses({body}) { - if (!body) { - return []; - } - - const {seatbid, cur} = body; - - if (!seatbid.length || !seatbid[0].bid || !seatbid[0].bid.length) { - return []; - } - - return { - bidResponses: seatbid[0].bid, - cur - }; -} - -function getBid(bids, currency, bidResponse) { - if (!bidResponse) { - return; - } - let { - price: cpm, nurl, crid: creativeId, adm: ad, w: width, h: height, exp: ttl, adomain: advertiserDomains, meta = {} - } = bidResponse; - let requestId = bids[bidResponse.impid - 1].bidId; - if (advertiserDomains && advertiserDomains.length > 0) { - meta.advertiserDomains = advertiserDomains - } - - ad = replaceAuctionPrice(ad, cpm); - - return { - requestId, - ttl, - mediaType: BANNER, - cpm, - creativeId, - currency, - ad, - width, - height, - meta, - nurl, - netRevenue: true - }; -} - registerBidder(spec); diff --git a/modules/tagorasBidAdapter.js b/modules/tagorasBidAdapter.js new file mode 100644 index 00000000000..0138ba3daf9 --- /dev/null +++ b/modules/tagorasBidAdapter.js @@ -0,0 +1,342 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'tagoras'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.tagoras.io`; +} + +export function extractCID(params) { + return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; +} + +export function extractPID(params) { + return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; +} + +export function extractSubDomain(params) { + return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.tagoras.io/api/sync/iframe/${params}` + }); + } else if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.tagoras.io/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/tagorasBidAdapter.md b/modules/tagorasBidAdapter.md new file mode 100644 index 00000000000..83290bff525 --- /dev/null +++ b/modules/tagorasBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Tagoras Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** prebid@tagoras.io + +# Description + +Module that connects to Tagoras's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'tagoras', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index 531927114c8..898f6a4185d 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -53,7 +53,7 @@ export const spec = { * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. - */ + */ isBidRequestValid: function(bid) { // bid.params.host if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) { // New endpoint @@ -234,12 +234,12 @@ function interpretBid(serverBid, request) { } /** -* Build and makes the request -* -* @param {*} validBidRequests -* @param {*} bidderRequest -* @return response ad -*/ + * Build and makes the request + * + * @param {*} validBidRequests + * @param {*} bidderRequest + * @return response ad + */ function buildOneRequest(validBidRequests, bidderRequest) { let hostInfo = _getHostInfo(validBidRequests); const ENDPOINT = hostInfo.endpoint; @@ -276,7 +276,11 @@ function buildOneRequest(validBidRequests, bidderRequest) { site.name = bundle; site.page = bidderRequest?.refererInfo?.page || deepAccess(validBidRequests, 'params.site.page') || bidderRequest?.refererInfo?.topmostLocation || window.location.href || bundle; site.domain = bundle; - site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || ''; + try { + site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || ''; + } catch (e) { + site.ref = bidderRequest?.refererInfo?.ref || window.document.referrer || ''; + } site.ext = {}; site.ext.is_amp = bidderRequest?.refererInfo?.isAmp || 0; site.ext.page_da = deepAccess(validBidRequests, 'params.site.page') || '-'; diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 570ee51df9c..ba16c6ddf82 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, logError, deepAccess, getBidIdParameter, parseSizesInput, isArray } from '../src/utils.js'; +import {getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -45,6 +45,7 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { const bids = validBidRequests.map(buildRequestObject); + const topWindow = window.top; const payload = { referrer: getReferrerInfo(bidderRequest), @@ -55,6 +56,12 @@ export const spec = { timeToFirstByte: getTimeToFirstByte(window), data: bids, deviceWidth: screen.width, + screenOrientation: screen.orientation?.type, + historyLength: topWindow.history?.length, + viewportHeight: topWindow.visualViewport?.height, + viewportWidth: topWindow.visualViewport?.width, + hardwareConcurrency: topWindow.navigator?.hardwareConcurrency, + deviceMemory: topWindow.navigator?.deviceMemory, hb_version: '$prebid.version$', ...getSharedViewerIdParameters(validBidRequests), ...getFirstPartyTeadsIdParameter(validBidRequests) @@ -174,9 +181,13 @@ function getReferrerInfo(bidderRequest) { function getPageTitle() { try { - return window.top.document.title || document.title; + const ogTitle = window.top.document.querySelector('meta[property="og:title"]') + + return window.top.document.title || (ogTitle && ogTitle.content) || ''; } catch (e) { - return document.title; + const ogTitle = document.querySelector('meta[property="og:title"]') + + return document.title || (ogTitle && ogTitle.content) || ''; } } @@ -185,9 +196,7 @@ function getPageDescription() { try { element = window.top.document.querySelector('meta[name="description"]') || - window.top.document.querySelector('meta[property="og:description"]') || - document.querySelector('meta[name="description"]') || - document.querySelector('meta[property="og:description"]') + window.top.document.querySelector('meta[property="og:description"]') } catch (e) { element = document.querySelector('meta[name="description"]') || document.querySelector('meta[property="og:description"]') diff --git a/modules/teadsIdSystem.js b/modules/teadsIdSystem.js index b4067bf75c3..4744fd5d37b 100644 --- a/modules/teadsIdSystem.js +++ b/modules/teadsIdSystem.js @@ -34,9 +34,9 @@ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleNam /** @type {Submodule} */ export const teadsIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** * Vendor id of Teads @@ -44,21 +44,21 @@ export const teadsIdSubmodule = { */ gvlid: GVL_ID, /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{teadsId:string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{teadsId:string}} + */ decode(value) { return {teadsId: value} }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [submoduleConfig] - * @param {ConsentData} [consentData] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [submoduleConfig] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ getId(submoduleConfig, consentData) { const resp = function (callback) { const url = buildAnalyticsTagUrl(submoduleConfig, consentData); diff --git a/modules/temedyaBidAdapter.js b/modules/temedyaBidAdapter.js index 4eac0bc641f..cb9fe46d21a 100644 --- a/modules/temedyaBidAdapter.js +++ b/modules/temedyaBidAdapter.js @@ -12,20 +12,20 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, NATIVE], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function (bid) { return !!(bid.params.widgetId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -54,11 +54,11 @@ export const spec = { }); }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function (serverResponse, bidRequest) { try { const bidResponse = serverResponse.body; diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index 03f303498a1..def2446635b 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -159,7 +159,6 @@ export const spec = { withCredentials: true, }, bidder: 'theadx', - // TODO: is 'page' the right value here? referrer: encodeURIComponent(bidderRequest.refererInfo.page || ''), data: generatePayload(bidRequest, bidderRequest), mediaTypes: bidRequest['mediaTypes'], @@ -261,6 +260,7 @@ export const spec = { ad: creative, ttl: ttl || 3000, creativeId: bid.crid, + dealId: bid.dealid || null, netRevenue: true, currency: responseBody.cur, mediaType: mediaType, @@ -466,11 +466,19 @@ let generateImpBody = (bidRequest, bidderRequest) => { } else if (mediaTypes && mediaTypes.native) { native = generateNativeComponent(bidRequest, bidderRequest); } - const result = { id: bidRequest.index, tagid: bidRequest.params.tagId + '', }; + + // deals support + if (bidRequest.params.deals && Array.isArray(bidRequest.params.deals) && bidRequest.params.deals.length > 0) { + result.pmp = { + deals: bidRequest.params.deals, + private_auction: 0, + }; + } + if (banner) { result['banner'] = banner; } diff --git a/modules/theAdxBidAdapter.md b/modules/theAdxBidAdapter.md index 2392bfaa819..cdc7b8410a8 100644 --- a/modules/theAdxBidAdapter.md +++ b/modules/theAdxBidAdapter.md @@ -26,9 +26,9 @@ Module that connects to TheAdx demand sources { bidder: "theadx", params: { - pid: 1000, // publisher id - wid: 2000, //website id - tagId: 5000, //zone id + pid: 1, // publisher id + wid: 7, //website id + tagId: 19, //zone id } } ] @@ -43,9 +43,10 @@ Module that connects to TheAdx demand sources { bidder: "theadx", params: { - pid: 1000, // publisher id - wid: 2000, //website id - tagId: 5000, //zone id + pid: 1, // publisher id + wid: 7, //website id + tagId: 18, //zone id + deals:[{"id":"theadx:137"}] //optional } } ] @@ -80,9 +81,9 @@ Module that connects to TheAdx demand sources { bidder: "theadx", params: { - pid: 1000, // publisher id - wid: 2000, //website id - tagId: 5000, //zone id + pid: 1, // publisher id + wid: 7, //website id + tagId: 20, //zone id } } ] diff --git a/modules/timeoutRtdProvider.js b/modules/timeoutRtdProvider.js index 323a5291e2d..fe9597f6f4e 100644 --- a/modules/timeoutRtdProvider.js +++ b/modules/timeoutRtdProvider.js @@ -67,7 +67,7 @@ function getConnectionSpeed() { * Calculate the time to be added to the timeout * @param {Array} adUnits * @param {Object} rules - * @return {int} + * @return {number} */ function calculateTimeoutModifier(adUnits, rules) { logInfo('Timeout rules', rules); diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 65c5777acf3..617ce0ad6f4 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -160,7 +160,7 @@ export function getCachedTopics() { } /** - * Recieve messages from iframe loaded for bidders to fetch topic + * Receive messages from iframe loaded for bidders to fetch topic * @param {MessageEvent} evt */ export function receiveMessage(evt) { @@ -177,9 +177,9 @@ export function receiveMessage(evt) { } /** -Function to store Topics data recieved from iframe in storage(name: "prebid:topics") -* @param {Topics} topics -*/ +Function to store Topics data received from iframe in storage(name: "prebid:topics") + * @param {Topics} topics + */ export function storeInLocalStorage(bidder, topics) { const storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); const topicsObj = { @@ -202,8 +202,8 @@ function isCachedDataExpired(storedTime, cacheTime) { } /** -* Function to get random bidders based on count passed with array of bidders -**/ + * Function to get random bidders based on count passed with array of bidders + */ function getRandomBidders(arr, count) { return ([...arr].sort(() => 0.5 - Math.random())).slice(0, count) } diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index bac99d578c5..e3adb80fb2f 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -1,164 +1,241 @@ /* eslint-disable no-tabs */ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseUrl, deepAccess } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { getStorageManager } from '../src/storageManager.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; -export const ADAPTER_VERSION = '1'; -const SUPPORTED_AD_TYPES = [BANNER]; const BIDDER_CODE = 'tpmn'; -const URL = 'https://ad.tpmn.co.kr/prebidhb.tpmn'; -const IFRAMESYNC = 'https://ad.tpmn.co.kr/sync.tpmn?type=iframe'; -export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const DEFAULT_BID_TTL = 500; +const DEFAULT_CURRENCY = 'USD'; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +// const BIDDER_ENDPOINT_URL = 'http://localhost:8081/ortb/pbjs_bidder'; +const BIDDER_ENDPOINT_URL = 'https://gat.tpmn.io/ortb/pbjs_bidder'; +const IFRAMESYNC = 'https://gat.tpmn.io/sync/iframe'; +const COMMON_PARAMS = [ + 'battr' +]; +export const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +export const ADAPTER_VERSION = '2.0'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, /** - *Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return 'params' in bid && - 'inventoryId' in bid.params && - 'publisherId' in bid.params && - !isNaN(Number(bid.params.inventoryId)) && - bid.params.inventoryId > 0 && - (typeof bid.mediaTypes.banner.sizes != 'undefined'); // only accepting appropriate sizes - }, - - /** - * @param {BidRequest[]} bidRequests - * @param {*} bidderRequest - * @return {ServerRequest} + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid The bid that won the auction */ - buildRequests: (bidRequests, bidderRequest) => { - if (bidRequests.length === 0) { - return []; + onBidWon: function (bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl); } - const bids = bidRequests.map(bidToRequest); - const bidderApiUrl = URL; - const payload = { - 'bids': [...bids], - 'site': createSite(bidderRequest.refererInfo) - }; - return [{ - method: 'POST', - url: bidderApiUrl, - data: payload - }]; + } +} + +function isBidRequestValid(bid) { + return (isValidInventoryId(bid) && (isValidBannerRequest(bid) || isValidVideoRequest(bid))); +} + +function isValidInventoryId(bid) { + return 'params' in bid && 'inventoryId' in bid.params && utils.isNumber(bid.params.inventoryId); +} + +function isValidBannerRequest(bid) { + const bannerSizes = utils.deepAccess(bid, `mediaTypes.${BANNER}.sizes`); + return utils.isArray(bannerSizes) && bannerSizes.length > 0 && bannerSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function isValidVideoRequest(bid) { + const videoSizes = utils.deepAccess(bid, `mediaTypes.${VIDEO}.playerSize`); + const videoMimes = utils.deepAccess(bid, `mediaTypes.${VIDEO}.mimes`); + + const isValidVideoSize = utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); + const isValidVideoMimes = utils.isArray(videoMimes) && videoMimes.length > 0; + return isValidVideoSize && isValidVideoMimes; +} + +function buildRequests(validBidRequests, bidderRequest) { + let requests = []; + try { + if (validBidRequests.length === 0 || !bidderRequest) return []; + let bannerBids = validBidRequests.filter(bid => utils.deepAccess(bid, 'mediaTypes.banner')); + let videoBids = validBidRequests.filter(bid => utils.deepAccess(bid, 'mediaTypes.video')); + + bannerBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, BANNER)); + }); + + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + } catch (err) { + utils.logWarn('buildRequests', err); + } + + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const rtbData = CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + + const bid = bidRequests.find((b) => b.params.inventoryId) + + if (bid.params.inventoryId) rtbData.ext = {}; + if (bid.params.inventoryId) rtbData.ext.inventoryId = bid.params.inventoryId + + const ortb2Data = bidderRequest?.ortb2 || {}; + const bcat = ortb2Data?.bcat || bid.params.bcat || []; + const badv = ortb2Data?.badv || bid.params.badv || []; + const bapp = ortb2Data?.bapp || bid.params.bapp || []; + + if (bcat.length > 0) { + rtbData.bcat = bcat; + } + if (badv.length > 0) { + rtbData.badv = badv; + } + if (badv.length > 0) { + rtbData.bapp = bapp; + } + + return { + method: 'POST', + url: BIDDER_ENDPOINT_URL + '?v=' + ADAPTER_VERSION, + data: rtbData + } +} + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; +} + +registerBidder(spec); + +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {serverResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: function (serverResponse, serverRequest) { - if (!Array.isArray(serverResponse.body)) { - return []; + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; } - // server response body is an array of bid results - const bidResults = serverResponse.body; - // our server directly returns the format needed by prebid.js so no more - // transformation is needed here. - return bidResults; - }, - - getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { - const syncArr = []; - if (syncOptions.iframeEnabled) { - let policyParam = ''; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + [VIDEO, BANNER].forEach(namespace => { + COMMON_PARAMS.forEach(param => { + if (bidRequest.params.hasOwnProperty(param)) { + utils.deepSetValue(imp, `${namespace}.${param}`, bidRequest.params[param]) } + }) + }) + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + const bidResponse = buildBidResponse(bid, context); + if (bidResponse.mediaType === BANNER) { + bidResponse.ad = bid.adm; + } else if (bidResponse.mediaType === VIDEO) { + if (bidRequest.mediaTypes.video.context === 'outstream') { + bidResponse.rendererUrl = VIDEO_RENDERER_URL; + bidResponse.renderer = createRenderer(bidRequest); } - if (uspConsent && uspConsent.consentString) { - policyParam += `&ccpa_consent=${uspConsent.consentString}`; - } - const coppa = config.getConfig('coppa') ? 1 : 0; - policyParam += `&coppa=${coppa}`; - syncArr.push({ - type: 'iframe', - url: IFRAMESYNC + policyParam - }); - } else { - syncArr.push({ - type: 'image', - url: 'https://x.bidswitch.net/sync?ssp=tpmn' - }); - syncArr.push({ - type: 'image', - url: 'https://gocm.c.appier.net/tpmn' - }); - syncArr.push({ - type: 'image', - url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' - }); - syncArr.push({ - type: 'image', - url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' - }); } - return syncArr; + return bidResponse; }, -}; + overrides: { + imp: { + video(orig, imp, bidRequest, context) { + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + }, + }, + } +}); -registerBidder(spec); +function createRenderer(bid) { + const renderer = Renderer.install({ + id: bid.bidId, + url: VIDEO_RENDERER_URL, + config: utils.deepAccess(bid, 'renderer.options'), + loaded: false, + adUnitCode: bid.adUnitCode + }); -/** - * Creates site description object - */ -function createSite(refInfo) { - let url = parseUrl(refInfo.page || ''); - let site = { - 'domain': url.hostname, - 'page': url.protocol + '://' + url.hostname + url.pathname - }; - if (refInfo.ref) { - site.ref = refInfo.ref - } - let keywords = document.getElementsByTagName('meta')['keywords']; - if (keywords && keywords.content) { - site.keywords = keywords.content; + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); } - return site; + return renderer; } -function parseSize(size) { - let sizeObj = {} - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - return sizeObj; -} +function outstreamRender(bid, doc) { + bid.renderer.push(() => { + const win = (doc) ? doc.defaultView : window; + win.ANOutstreamVideo.renderAd({ + sizes: [bid.playerWidth, bid.playerHeight], + targetId: bid.adUnitCode, + rendererOptions: bid.renderer.getConfig(), + adResponse: { content: bid.vastXml } -function parseSizes(sizes) { - if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) - return sizes.map(size => parseSize(size)); - } - return [parseSize(sizes)]; // or a single one ? (ie. [728,90]) + }, handleOutstreamRendererEvents.bind(null, bid)); + }); } -function getBannerSizes(bidRequest) { - return parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes); +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); } -function bidToRequest(bid) { - const bidObj = {}; - bidObj.sizes = getBannerSizes(bid); - - bidObj.inventoryId = bid.params.inventoryId; - bidObj.publisherId = bid.params.publisherId; - bidObj.bidId = bid.bidId; - bidObj.adUnitCode = bid.adUnitCode; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - bidObj.auctionId = bid.auctionId; - - return bidObj; +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncArr = []; + if (syncOptions.iframeEnabled) { + let policyParam = ''; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + policyParam += `&ccpa_consent=${uspConsent.consentString}`; + } + const coppa = config.getConfig('coppa') ? 1 : 0; + policyParam += `&coppa=${coppa}`; + syncArr.push({ + type: 'iframe', + url: IFRAMESYNC + '?' + policyParam + }); + } else { + syncArr.push({ + type: 'image', + url: 'https://x.bidswitch.net/sync?ssp=tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://gocm.c.appier.net/tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' + }); + syncArr.push({ + type: 'image', + url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' + }); + } + return syncArr; } diff --git a/modules/tpmnBidAdapter.md b/modules/tpmnBidAdapter.md index 8387528bb0f..3b016d7e5b2 100644 --- a/modules/tpmnBidAdapter.md +++ b/modules/tpmnBidAdapter.md @@ -11,10 +11,27 @@ Maintainer: develop@tpmn.co.kr Connects to TPMN exchange for bids. NOTE: -- TPMN bid adapter only supports Banner at the moment. +- TPMN bid adapter only supports MediaType BANNER, VIDEO. - Multi-currency is not supported. +- Please contact the TPMN sales team via email for "inventoryId" issuance. -# Sample Ad Unit Config + +# Bid Parameters + +## bids.params (Banner, Video) +***Pay attention to the case sensitivity.*** + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| -------------- | ----------- | ------------------------------------------ | ------------- | ------------ | +| `inventoryId` | required | Ad Inventory id TPMN | 123 | Number | +| `bidFloor` | recommended | Minimum price in USD. bidFloor applies to a specific unit. | 1.50 | Number | +| `bcat` | optional | IAB 5.1 Content Categories | ['IAB7-39'] | [String] | +| `badv` | optional | IAB Block list of advertisers by their domains | ['example.com'] | [String] | +| `bapp` | optional | IAB Block list of applications | ['com.blocked'] | [String] | + + +# Banner Ad Unit Config ``` var adUnits = [{ // Banner adUnit @@ -22,16 +39,77 @@ NOTE: mediaTypes: { banner: { sizes: [[300, 250], [320, 50]], // banner size + battr: [1,2,3] // optional } }, bids: [ { bidder: 'tpmn', params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 1, // required + bidFloor: 2.0, // recommended + ... // bcat, badv, bapp // optional } } ] }]; +``` + + +# mediaTypes Parameters + +## mediaTypes.banner + +The following banner parameters are supported here so publishers may fully declare their banner inventory: + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| `sizes` | required | Avalaible sizes supported for banner ad unit | [ [300, 250], [300, 600] ] | [[Integer, Integer], [Integer, Integer]] | +| `battr` | optional | IAB 5.3 Creative Attributes | [1,2,3] | [Number] | +## mediaTypes.video + +We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video` + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| `context` | required | instream or outstream |'outstream' | string | +| `playerSize` | required | Avalaible sizes supported for video ad unit. | [[300, 250]] | [Integer, Integer] | +| `mimes` | required | List of content MIME types supported by the player. | ['video/mp4']| [String]| +| `protocols` | optional | Supported video bid response protocol values. | [2,3,5,6] | [integers]| +| `api` | optional | Supported API framework values. | [2] | [integers] | +| `maxduration` | optional | Maximum video ad duration in seconds. | 30 | Integer | +| `minduration` | optional | Minimum video ad duration in seconds. | 6 | Integer | +| `startdelay` | optional | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | 0 | Integer | +| `placement` | optional | Placement type for the impression. | 1 | Integer | +| `minbitrate` | optional | Minimum bit rate in Kbps. | 300 | Integer | +| `maxbitrate` | optional | Maximum bit rate in Kbps. | 9600 | Integer | +| `playbackmethod` | optional | Playback methods that may be in use. Only one method is typically used in practice. | [2]| [Integers] | +| `linearity` | optional | OpenRTB2 linearity. in-strea,overlay... | 1 | Integer | +| `skip` | optional | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes . | 1 | Integer | +| `battr` | optional | IAB 5.3 Creative Attributes | [1,2,3] | [Number] | + + +# Video Ad Unit Config +``` + var adUnits = [{ + code: 'video-div', + mediaTypes: { + video: { + context: 'instream', // required + mimes: ['video/mp4'], // required + playerSize: [[ 640, 480 ]], // required + ... // skippable, startdelay, battr.. // optional + } + }, + bids: [{ + bidder: 'tpmn', + params: { + inventoryId: 2, // required + bidFloor: 2.0, // recommended + ... // bcat, badv, bapp // optional + } + }] + }]; ``` \ No newline at end of file diff --git a/modules/trafficgateBidAdapter.js b/modules/trafficgateBidAdapter.js index 25289a19801..fcd84306099 100644 --- a/modules/trafficgateBidAdapter.js +++ b/modules/trafficgateBidAdapter.js @@ -1,7 +1,8 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -import {deepAccess, mergeDeep, convertTypes} from '../src/utils.js'; +import {deepAccess, mergeDeep} from '../src/utils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'trafficgate'; const URL = 'https://[HOST].bc-plugin.com/prebidjs' diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index b6375243a5a..e976396c71c 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -1,6 +1,7 @@ -import { getBidIdParameter, parseSizesInput, tryAppendQueryString } from '../src/utils.js'; +import {getBidIdParameter, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index a4aeedd0cfc..bfbf1409c1b 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -1,8 +1,9 @@ -import { tryAppendQueryString, logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; +import { logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const GVLID = 28; const BIDDER_CODE = 'triplelift'; @@ -239,7 +240,12 @@ function _getORTBVideo(bidRequest) { } catch (err) { logWarn('Video size not defined', err); } - if (video.context === 'instream') video.placement = 1; + // honor existing publisher settings + if (video.context === 'instream') { + if (!video.placement) { + video.placement = 1; + } + } if (video.context === 'outstream') { if (!video.placement) { video.placement = 3 diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js index bbe207abc9e..17a3cd652e8 100644 --- a/modules/ttdBidAdapter.js +++ b/modules/ttdBidAdapter.js @@ -4,7 +4,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import {isNumber} from '../src/utils.js'; -const BIDADAPTERVERSION = 'TTD-PREBID-2022.06.28'; +const BIDADAPTERVERSION = 'TTD-PREBID-2023.09.05'; const BIDDER_CODE = 'ttd'; const BIDDER_CODE_LONG = 'thetradedesk'; const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; @@ -406,7 +406,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { const firstPartyData = bidderRequest.ortb2 || {}; let topLevel = { - id: bidderRequest.auctionId, + id: bidderRequest.bidderRequestId, imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), site: getSite(bidderRequest, firstPartyData), device: getDevice(firstPartyData), diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 2225deaa900..1ef72f293fc 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -64,11 +64,10 @@ export const spec = { * Format ucfunnel responses as Prebid bid responses * @param {ucfunnelResponseObj} ucfunnelResponse A successful response from ucfunnel. * @return {Bid[]} An array of formatted bids. - */ + */ interpretResponse: function (ucfunnelResponseObj, request) { const bidRequest = request.bidRequest; const ad = ucfunnelResponseObj ? ucfunnelResponseObj.body : {}; - const videoPlayerSize = parseSizes(bidRequest); let bid = { requestId: bidRequest.bidId, @@ -117,10 +116,10 @@ export const spec = { vastXml: ad.vastXml }); - if (videoPlayerSize && videoPlayerSize.length === 2) { + if (bidRequest.sizes && bidRequest.sizes.length > 0) { Object.assign(bid, { - width: videoPlayerSize[0], - height: videoPlayerSize[1] + width: bidRequest.sizes[0][0], + height: bidRequest.sizes[0][1] }); } break; @@ -128,8 +127,8 @@ export const spec = { default: var size = parseSizes(bidRequest); Object.assign(bid, { - width: ad.width || size[0], - height: ad.height || size[1], + width: ad.width || size[0][0], + height: ad.height || size[0][1], ad: ad.adm || '' }); } @@ -156,12 +155,6 @@ export const spec = { }; registerBidder(spec); -function transformSizes(requestSizes) { - if (typeof requestSizes === 'object' && requestSizes.length) { - return requestSizes[0]; - } -} - function getCookieSyncParameter(gdprApplies, apiVersion, consentString, uspConsent) { let param = '?'; if (gdprApplies == '1') { @@ -187,11 +180,10 @@ function parseSizes(bid) { params.video.playerWidth, params.video.playerHeight ]; - return size; + return [size]; } } - - return transformSizes(bid.sizes); + return bid.sizes; } function getSupplyChain(schain) { @@ -244,6 +236,20 @@ function getFloor(bid, size, mediaTypes) { return undefined; } +function addBidData(bidData, key, value) { + if (value) { + bidData[key] = value; + } +} + +function getFormat(size) { + let formatList = [] + for (var i = 0; i < size.length; i++) { + formatList.push(size[i].join(',')); + } + return (formatList.length > 0) ? formatList.join(';') : ''; +} + function getRequestData(bid, bidderRequest) { const size = parseSizes(bid); const language = navigator.language; @@ -264,14 +270,8 @@ function getRequestData(bid, bidderRequest) { schain: supplyChain }; - if (bidFloor) { - bidData.fp = bidFloor; - } - - if (gpid) { - bidData.gpid = gpid; - } - + addBidData(bidData, 'fp', bidFloor); + addBidData(bidData, 'gpid', gpid); addUserId(bidData, bid.userId); bidData.u = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; @@ -293,10 +293,11 @@ function getRequestData(bid, bidderRequest) { } } - if (size != undefined && size.length == 2) { - bidData.w = size[0]; - bidData.h = size[1]; + if (size != undefined && size.length > 0 && size[0].length == 2) { + bidData.w = size[0][0]; + bidData.h = size[0][1]; } + addBidData(bidData, 'format', getFormat(size)); if (bidderRequest && bidderRequest.uspConsent) { Object.assign(bidData, { diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index a36b24e3ae3..13c0303e0b3 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -7,13 +7,14 @@ */ import { logInfo, logWarn } from '../src/utils.js'; -import {submodule} from '../src/hook.js'; +import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // RE below lint exception: UID2 and EUID are separate modules, but the protocol is the same and shared code makes sense here. // eslint-disable-next-line prebid/validate-imports -import { Uid2GetId, Uid2CodeVersion } from './uid2IdSystem_shared.js'; +import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; const MODULE_NAME = 'uid2'; const MODULE_REVISION = Uid2CodeVersion; @@ -32,6 +33,7 @@ function createLogger(logger, prefix) { logger(prefix + ' ', ...strings); } } + const _logInfo = createLogger(logInfo, LOG_PRE_FIX); const _logWarn = createLogger(logWarn, LOG_PRE_FIX); @@ -78,25 +80,20 @@ export const uid2IdSubmodule = { clientId: UID2_CLIENT_ID, internalStorage: ADVERTISING_COOKIE } + + if (FEATURES.UID2_CSTG) { + mappedConfig.cstg = { + serverPublicKey: config?.params?.serverPublicKey, + subscriptionId: config?.params?.subscriptionId, + ...extractIdentityFromParams(config?.params ?? {}) + } + } _logInfo(`UID2 configuration loaded and mapped.`, mappedConfig); const result = Uid2GetId(mappedConfig, storage, _logInfo, _logWarn); _logInfo(`UID2 getId returned`, result); return result; }, - eids: { - 'uid2': { - source: 'uidapi.com', - atype: 3, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - }, + eids: UID2_EIDS }; function decodeImpl(value) { diff --git a/modules/uid2IdSystem.md b/modules/uid2IdSystem.md index a795d9b1aa1..e546f6eafe1 100644 --- a/modules/uid2IdSystem.md +++ b/modules/uid2IdSystem.md @@ -1,11 +1,63 @@ ## UID2 User ID Submodule -UID2 requires initial tokens to be generated server-side. The UID2 module handles storing, providing, and optionally refreshing them. The module can operate in one of two different modes: *Client Refresh* mode or *Server Only* mode. +The UID2 module handles storing, providing, and optionally refreshing tokens. While initial tokens traditionally required server-side generation, the introduction of the *Client-Side Token Generation (CSTG)* mode offers publishers the flexibility to generate UID2 tokens directly from the module, eliminating this need. Publishers can choose to operate the module in one of three distinct modes: *Client Refresh* mode, *Server Only* mode and *Client-Side Token Generation* mode. *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. +*Client-Side Token Generation* mode is included in UID2 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: + +``` + $ gulp build --modules=uid2IdSystem --disable UID2_CSTG +``` +If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the UID2 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) + **Important information:** UID2 is not designed to be used where GDPR applies. The module checks the passed-in consent data and will not operate if the `gdprApplies` flag is true. +## Client-Side Token Generation (CSTG) mode + +**This mode is created and made available recently. Please consult UID2 Team first as they will provide required configuration values for you to use.** + +For publishers seeking a purely client-side integration without the complexities of server-side involvement, the CSTG mode is highly recommended. This mode requires the provision of a public key, subscription ID and [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) - either emails or phone numbers. In the CSTG mode, the module takes on the responsibility of encrypting the DII, generating the UID2 token, and handling token refreshes when necessary. + +To configure the module to use this mode, you must: +1. Set `parmas.serverPublicKey` and `params.subscriptionId` (please reach out to the UID2 team to obtain these values) +2. Provide **ONLY ONE DII** by setting **ONLY ONE** of `params.email`/`params.phone`/`params.emailHash`/`params.phoneHash` + +Below is a table that provides guidance on when to use each directly identifying information (DII) parameter, along with information on whether normalization and hashing are required by the publisher for each parameter. + +| DII param | When to use it | Normalization required by publisher? | Hashing required by publisher? | +|------------------|-------------------------------------------------------|--------------------------------------|--------------------------------| +| params.email | When you have users' email address | No | No | +| params.phone | When you have user's phone number | Yes | No | +| params.emailHash | When you have user's hashed, normalized email address | Yes | Yes | +| params.phoneHash | When you have user's hashed, normalized phone number | Yes | Yes | + + +*Note that setting params.email will normalize email addresses, but params.phone requires phone numbers to be normalized.* + +Refer to [Normalization and Encoding](#normalization-and-encoding) for details on email address normalization, SHA-256 hashing and Base64 encoding. + +### CSTG example + +Configuration: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'uid2', + params: { + serverPublicKey: '...server public key...', + subscriptionId: '...subcription id...', + email: 'user@email.com', + //phone: '+0000000', + //emailHash: '...email hash...', + //phoneHash: '...phone hash ...' + } + }] + } +}); +``` + ## Client Refresh mode This is the recommended mode for most scenarios. In this mode, the full response body from the UID2 Token Generate or Token Refresh endpoint must be provided to the module. As long as the refresh token remains valid, the module will refresh the advertising token as needed. @@ -133,3 +185,80 @@ The below parameters apply only to the UID2 User ID Module integration. | params.uid2Cookie | Optional, Client refresh | String | The name of a cookie which holds the initial UID2 token, set by the server. The cookie should contain JSON in the same format as the uid2Token param. **If uid2Token is supplied, this param is ignored.** | See the sample token above. | | params.uid2ApiBase | Optional, Client refresh | String | Overrides the default UID2 API endpoint. | `"https://prod.uidapi.com"` _(default)_| | params.storage | Optional, Client refresh | String | Specify whether to use `cookie` or `localStorage` for module-internal storage. It is recommended to not provide this and allow the module to use the default. | `localStorage` _(default)_ | +| params.serverPublicKey | Optional, Client-side token generation | String | A public key for encrypting the DII payload for the Operator's CSTG endpoint. **This is required for client-side token generation.** | - | +| params.subscriptionId | Optional, Client-side token generation | String | A publisher Identifier. **This is required for client-side token generation.** | - | +| params.email | Optional, Client-side token generation | String | The user's email address. Provide this parameter if using email as the DII. | `"test@example.com"` | +| params.emailHash | Optional, Client-side token generation | String | A hashed, normalized representation of the user's email. Provide this parameter if using emailHash as the DII. | `"tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ="` | +| params.phone | Optional, Client-side token generation | String | The user's phone number. Provide this parameter if using phone as the DII. | `"+15555555555"` | +| params.phoneHash | Optional, Client-side token generation | String | A hashed, normalized representation of the user's phone number. Provide this parameter if using phoneHash as the DII. | `"tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ="` | + +# Normalization and Encoding + +This section provides information about normalizing and encoding [directly Identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii). It's important that, in working with UID2, normalizing and encoding are performed correctly. + +## Introduction +When you're taking user information such as an email address, and following the steps to create a raw UID2 and/or a UID2 advertising token, it's very important that you follow all the required steps. Whether you normalize the information or not, whether you hash it or not, follow the steps exactly. By doing so, you can ensure that the UID2 value you create can be securely and anonymously matched up with other instances of online behavior by the same user. + +>Note: Raw UID2s, and their associated UID2 tokens, are case sensitive. When working with UID2, it's important to pass all IDs and tokens without changing the case. Mismatched IDs can cause ID parsing or token decryption errors. + +## Types of Directly Identifying Information +UID2 supports the following types of directly identifying information (DII): +- Email address +- Phone number + +## Email Address Normalization + +If you send unhashed email addresses to the UID2 Operator Service, the service normalizes the email addresses and then hashes them. If you want to hash the email addresses yourself before sending them, you must normalize them before you hash them. + +> IMPORTANT: Normalizing before hashing ensures that the generated UID2 value will always be the same, so that the data can be matched. If you do not normalize before hashing, this might result in a different UID2, reducing the effectiveness of targeted advertising. + +To normalize an email address, complete the following steps: + +1. Remove leading and trailing spaces. +2. Convert all ASCII characters to lowercase. +3. In `gmail.com` email addresses, remove the following characters from the username part of the email address: + 1. The period (`.` (ASCII code 46)).
For example, normalize `jane.doe@gmail.com` to `janedoe@gmail.com`. + 2. The plus sign (`+` (ASCII code 43)) and all subsequent characters.
For example, normalize `janedoe+home@gmail.com` to `janedoe@gmail.com`. + +## Email Address Hash Encoding + +An email hash is a Base64-encoded SHA-256 hash of a normalized email address. The email address is first normalized, then hashed using the SHA-256 hashing algorithm, and then the resulting bytes of the hash value are encoded using Base64 encoding. Note that the bytes of the hash value are encoded, not the hex-encoded string representation. + +| Type | Example | Comments and Usage | +| :--- | :--- | :--- | +| Normalized email address | `user@example.com` | Normalization is always the first step. | +| SHA-256 hash of normalized email address | `b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514` | This 64-character string is a hex-encoded representation of the 32-byte SHA-256.| +| Hex to Base64 SHA-256 encoding of normalized email address | `tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ=` | This 44-character string is a Base64-encoded representation of the 32-byte SHA-256.
WARNING: The SHA-256 hash string in the example above is a hex-encoded representation of the hash value. You must Base64-encode the raw bytes of the hash or use a Base64 encoder that takes a hex-encoded value as input.
Use this encoding for `email_hash` values sent in the request body. | + +>WARNING: When applying Base64 encoding, be sure to Base64-encode the raw bytes of the hash or use a Base64 encoder that takes a hex-encoded value as input. + +## Phone Number Normalization + +If you send unhashed phone numbers to the UID2 Operator Service, the service normalizes the phone numbers and then hashes them. If you want to hash the phone numbers yourself before sending them, you must normalize them before you hash them. + +> IMPORTANT: Normalization before hashing ensures that the generated UID2 value will always be the same, so that the data can be matched. If you do not normalize before hashing, this might result in a different UID2, reducing the effectiveness of targeted advertising. + +Here's what you need to know about phone number normalization rules: + +- The UID2 Operator accepts phone numbers in the [E.164](https://en.wikipedia.org/wiki/E.164) format, which is the international phone number format that ensures global uniqueness. +- E.164 phone numbers can have a maximum of 15 digits. +- Normalized E.164 phone numbers use the following syntax, with no spaces, hyphens, parentheses, or other special characters:
+ `[+] [country code] [subscriber number including area code]` + Examples: + - US: `1 (123) 456-7890` is normalized to `+11234567890`. + - Singapore: `65 1243 5678` is normalized to `+6512345678`. + - Sydney, Australia: `(02) 1234 5678` is normalized to drop the leading zero for the city plus include the country code: `+61212345678`. + +## Phone Number Hash Encoding + +A phone number hash is a Base64-encoded SHA-256 hash of a normalized phone number. The phone number is first normalized, then hashed using the SHA-256 hashing algorithm, and the resulting hex value is encoded using Base64 encoding. + +The example below shows a simple input phone number, and the result as each step is applied to arrive at a secure, opaque, URL-safe value. + +| Type | Example | Comments and Usage | +| :--- | :--- | :--- | +| Normalized phone number | `+12345678901` | Normalization is always the first step. | +| SHA-256 hash of normalized phone number | `10e6f0b47054a83359477dcb35231db6de5c69fb1816e1a6b98e192de9e5b9ee` |This 64-character string is a hex-encoded representation of the 32-byte SHA-256. | +| Hex to Base64 SHA-256 encoding of normalized and hashed phone number | `EObwtHBUqDNZR33LNSMdtt5cafsYFuGmuY4ZLenlue4=` | This 44-character string is a Base64-encoded representation of the 32-byte SHA-256.
NOTE: The SHA-256 hash is a hexadecimal value. You must use a Base64 encoder that takes a hex value as input. Use this encoding for `phone_hash` values sent in the request body. | + +>WARNING: When applying Base64 encoding, be sure to use a function that takes a hex value as input. If you use a function that takes text as input, the result is a longer string which is invalid for the purposes of UID2. diff --git a/modules/uid2IdSystem_shared.js b/modules/uid2IdSystem_shared.js index 0f6894a9d3e..acc440eafc5 100644 --- a/modules/uid2IdSystem_shared.js +++ b/modules/uid2IdSystem_shared.js @@ -1,4 +1,7 @@ /* eslint-disable no-console */ +import { ajax } from '../src/ajax.js'; +import { cyrb53Hash } from '../src/utils.js'; + export const Uid2CodeVersion = '1.1'; function isValidIdentity(identity) { @@ -13,6 +16,7 @@ export class Uid2ApiClient { this._logInfo = logInfo; this._logWarn = logWarn; } + createArrayBuffer(text) { const arrayBuffer = new Uint8Array(text.length); for (let i = 0; i < text.length; i++) { @@ -36,49 +40,58 @@ export class Uid2ApiClient { } callRefreshApi(refreshDetails) { const url = this._baseUrl + '/v2/token/refresh'; - const req = new XMLHttpRequest(); - req.overrideMimeType('text/plain'); - req.open('POST', url, true); - req.setRequestHeader('X-UID2-Client-Version', this._clientVersion); let resolvePromise; let rejectPromise; const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); - req.onreadystatechange = () => { - if (req.readyState !== req.DONE) { return; } - try { - if (!refreshDetails.refresh_response_key || req.status !== 200) { - this._logInfo('Error status OR no response decryption key available, assuming unencrypted JSON'); - const response = JSON.parse(req.responseText); + this._logInfo('Sending refresh request', refreshDetails); + ajax(url, { + success: (responseText) => { + try { + if (!refreshDetails.refresh_response_key) { + this._logInfo('No response decryption key available, assuming unencrypted JSON'); + const response = JSON.parse(responseText); + const result = this.ResponseToRefreshResult(response); + if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + } else { + this._logInfo('Decrypting refresh API response'); + const encodeResp = this.createArrayBuffer(atob(responseText)); + window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => { + this._logInfo('Imported decryption key') + // returns the symmetric key + window.crypto.subtle.decrypt({ + name: 'AES-GCM', + iv: encodeResp.slice(0, 12), + tagLength: 128, // The tagLength you used to encrypt (if any) + }, key, encodeResp.slice(12)).then((decrypted) => { + const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); + this._logInfo('Decrypted to:', decryptedResponse); + const response = JSON.parse(decryptedResponse); + const result = this.ResponseToRefreshResult(response); + if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + } + } catch (_err) { + rejectPromise(responseText); + } + }, + error: (error, xhr) => { + try { + this._logInfo('Error status, assuming unencrypted JSON'); + const response = JSON.parse(xhr.responseText); const result = this.ResponseToRefreshResult(response); if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - } else { - this._logInfo('Decrypting refresh API response'); - const encodeResp = this.createArrayBuffer(atob(req.responseText)); - window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => { - this._logInfo('Imported decryption key') - // returns the symmetric key - window.crypto.subtle.decrypt({ - name: 'AES-GCM', - iv: encodeResp.slice(0, 12), - tagLength: 128, // The tagLength you used to encrypt (if any) - }, key, encodeResp.slice(12)).then((decrypted) => { - const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); - this._logInfo('Decrypted to:', decryptedResponse); - const response = JSON.parse(decryptedResponse); - const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + } catch (_e) { + rejectPromise(error) } - } catch (err) { - rejectPromise(err); } - }; - this._logInfo('Sending refresh request', refreshDetails); - req.send(refreshDetails.refresh_token); + }, refreshDetails.refresh_token, { method: 'POST', + customHeaders: { + 'X-UID2-Client-Version': this._clientVersion + } }); return promise; } } @@ -160,18 +173,507 @@ function refreshTokenAndStore(baseUrl, token, clientId, storageManager, _logInfo originalToken: token, latestToken: response.identity, }; + let storedTokens = storageManager.getStoredValueWithFallback(); + if (storedTokens?.originalIdentity) tokens.originalIdentity = storedTokens.originalIdentity; storageManager.storeValue(tokens); return tokens; }); } +let clientSideTokenGenerator; +if (FEATURES.UID2_CSTG) { + const SERVER_PUBLIC_KEY_PREFIX_LENGTH = 9; + + clientSideTokenGenerator = { + isCSTGOptionsValid(maybeOpts, _logWarn) { + if (typeof maybeOpts !== 'object' || maybeOpts === null) { + _logWarn('CSTG opts must be an object'); + return false; + } + + const opts = maybeOpts; + if (typeof opts.serverPublicKey !== 'string') { + _logWarn('CSTG opts.serverPublicKey must be a string'); + return false; + } + const serverPublicKeyPrefix = /^UID2-X-[A-Z]-.+/; + if (!serverPublicKeyPrefix.test(opts.serverPublicKey)) { + _logWarn( + `CSTG opts.serverPublicKey must match the regular expression ${serverPublicKeyPrefix}` + ); + return false; + } + // We don't do any further validation of the public key, as we will find out + // later if it's valid by using importKey. + + if (typeof opts.subscriptionId !== 'string') { + _logWarn('CSTG opts.subscriptionId must be a string'); + return false; + } + if (opts.subscriptionId.length === 0) { + _logWarn('CSTG opts.subscriptionId is empty'); + return false; + } + return true; + }, + + getValidIdentity(opts, _logWarn) { + if (opts.emailHash) { + if (!UID2DiiNormalization.isBase64Hash(opts.emailHash)) { + _logWarn('CSTG opts.emailHash is invalid'); + return; + } + return { email_hash: opts.emailHash }; + } + + if (opts.phoneHash) { + if (!UID2DiiNormalization.isBase64Hash(opts.phoneHash)) { + _logWarn('CSTG opts.phoneHash is invalid'); + return; + } + return { phone_hash: opts.phoneHash }; + } + + if (opts.email) { + const normalizedEmail = UID2DiiNormalization.normalizeEmail(opts.email); + if (normalizedEmail === undefined) { + _logWarn('CSTG opts.email is invalid'); + return; + } + return { email: normalizedEmail }; + } + + if (opts.phone) { + if (!UID2DiiNormalization.isNormalizedPhone(opts.phone)) { + _logWarn('CSTG opts.phone is invalid'); + return; + } + return { phone: opts.phone }; + } + }, + + isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn) { + if (storedTokens) { + const identity = Object.values(cstgIdentity)[0]; + if (!this.isStoredTokenFromSameIdentity(storedTokens, identity)) { + _logInfo( + 'CSTG supplied new identity - ignoring stored value.', + storedTokens.originalIdentity, + cstgIdentity + ); + // Stored token wasn't originally sourced from the provided identity - ignore the stored value. A new user has logged in? + return true; + } + } + return false; + }, + + async generateTokenAndStore( + baseUrl, + cstgOpts, + cstgIdentity, + storageManager, + _logInfo, + _logWarn + ) { + _logInfo('UID2 cstg opts provided: ', JSON.stringify(cstgOpts)); + const client = new UID2CstgApiClient( + { baseUrl, cstg: cstgOpts }, + _logInfo, + _logWarn + ); + const response = await client.generateToken(cstgIdentity); + _logInfo('CSTG endpoint responded with:', response); + const tokens = { + originalIdentity: this.encodeOriginalIdentity(cstgIdentity), + latestToken: response.identity, + }; + storageManager.storeValue(tokens); + return tokens; + }, + + isStoredTokenFromSameIdentity(storedTokens, identity) { + if (!storedTokens.originalIdentity) return false; + return ( + cyrb53Hash(identity, storedTokens.originalIdentity.salt) === + storedTokens.originalIdentity.identity + ); + }, + + encodeOriginalIdentity(identity) { + const identityValue = Object.values(identity)[0]; + const salt = Math.floor(Math.random() * Math.pow(2, 32)); + return { + identity: cyrb53Hash(identityValue, salt), + salt, + }; + }, + }; + + class UID2DiiNormalization { + static EMAIL_EXTENSION_SYMBOL = '+'; + static EMAIL_DOT = '.'; + static GMAIL_DOMAIN = 'gmail.com'; + + static isBase64Hash(value) { + if (!(value && value.length === 44)) { + return false; + } + + try { + return btoa(atob(value)) === value; + } catch (err) { + return false; + } + } + + static isNormalizedPhone(phone) { + return /^\+[0-9]{10,15}$/.test(phone); + } + + static normalizeEmail(email) { + if (!email || !email.length) return; + + const parsedEmail = email.trim().toLowerCase(); + if (parsedEmail.indexOf(' ') > 0) return; + + const emailParts = this.splitEmailIntoAddressAndDomain(parsedEmail); + if (!emailParts) return; + + const { address, domain } = emailParts; + + const emailIsGmail = this.isGmail(domain); + const parsedAddress = this.normalizeAddressPart( + address, + emailIsGmail, + emailIsGmail + ); + + return parsedAddress ? `${parsedAddress}@${domain}` : undefined; + } + + static splitEmailIntoAddressAndDomain(email) { + const parts = email.split('@'); + if ( + parts.length !== 2 || + parts.some((part) => part === '') + ) { return; } + + return { + address: parts[0], + domain: parts[1], + }; + } + + static isGmail(domain) { + return domain === this.GMAIL_DOMAIN; + } + + static dropExtension(address, extensionSymbol = this.EMAIL_EXTENSION_SYMBOL) { + return address.split(extensionSymbol)[0]; + } + + static normalizeAddressPart(address, shouldRemoveDot, shouldDropExtension) { + let parsedAddress = address; + if (shouldRemoveDot) { parsedAddress = parsedAddress.replaceAll(this.EMAIL_DOT, ''); } + if (shouldDropExtension) parsedAddress = this.dropExtension(parsedAddress); + return parsedAddress; + } + } + + class UID2CstgApiClient { + constructor(opts, logInfo, logWarn) { + this._baseUrl = opts.baseUrl; + this._serverPublicKey = opts.cstg.serverPublicKey; + this._subscriptionId = opts.cstg.subscriptionId; + this._logInfo = logInfo; + this._logWarn = logWarn; + } + + hasStatusResponse(response) { + return typeof response === 'object' && response && response.status; + } + + isCstgApiSuccessResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'success' && + isValidIdentity(response.body) + ); + } + + isCstgApiClientErrorResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'client_error' && + typeof response.message === 'string' + ); + } + + isCstgApiForbiddenResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'invalid_http_origin' && + typeof response.message === 'string' + ); + } + + stripPublicKeyPrefix(serverPublicKey) { + return serverPublicKey.substring(SERVER_PUBLIC_KEY_PREFIX_LENGTH); + } + + async generateCstgRequest(cstgIdentity) { + if ('email_hash' in cstgIdentity || 'phone_hash' in cstgIdentity) { + return cstgIdentity; + } + if ('email' in cstgIdentity) { + const emailHash = await UID2CstgCrypto.hash(cstgIdentity.email); + return { email_hash: emailHash }; + } + if ('phone' in cstgIdentity) { + const phoneHash = await UID2CstgCrypto.hash(cstgIdentity.phone); + return { phone_hash: phoneHash }; + } + } + + async generateToken(cstgIdentity) { + const request = await this.generateCstgRequest(cstgIdentity); + this._logInfo('Building CSTG request for', request); + const box = await UID2CstgBox.build( + this.stripPublicKeyPrefix(this._serverPublicKey) + ); + const encoder = new TextEncoder(); + const now = Date.now(); + const { iv, ciphertext } = await box.encrypt( + encoder.encode(JSON.stringify(request)), + encoder.encode(JSON.stringify([now])) + ); + + const exportedPublicKey = await UID2CstgCrypto.exportPublicKey( + box.clientPublicKey + ); + const requestBody = { + payload: UID2CstgCrypto.bytesToBase64(new Uint8Array(ciphertext)), + iv: UID2CstgCrypto.bytesToBase64(new Uint8Array(iv)), + public_key: UID2CstgCrypto.bytesToBase64( + new Uint8Array(exportedPublicKey) + ), + timestamp: now, + subscription_id: this._subscriptionId, + }; + return this.callCstgApi(requestBody, box); + } + + async callCstgApi(requestBody, box) { + const url = this._baseUrl + '/v2/token/client-generate'; + let resolvePromise; + let rejectPromise; + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + + this._logInfo('Sending CSTG request', requestBody); + ajax( + url, + { + success: async (responseText, xhr) => { + try { + const encodedResp = UID2CstgCrypto.base64ToBytes(responseText); + const decrypted = await box.decrypt( + encodedResp.slice(0, 12), + encodedResp.slice(12) + ); + const decryptedResponse = new TextDecoder().decode(decrypted); + const response = JSON.parse(decryptedResponse); + if (this.isCstgApiSuccessResponse(response)) { + resolvePromise({ + status: 'success', + identity: response.body, + }); + } else { + // A 200 should always be a success response. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 200: ${decryptedResponse}` + ); + } + } catch (err) { + rejectPromise(err); + } + }, + error: (error, xhr) => { + try { + if (xhr.status === 400) { + const response = JSON.parse(xhr.responseText); + if (this.isCstgApiClientErrorResponse(response)) { + rejectPromise(`Client error: ${response.message}`); + } else { + // A 400 should always be a client error. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 400: ${xhr.responseText}` + ); + } + } else if (xhr.status === 403) { + const response = JSON.parse(xhr.responseText); + if (this.isCstgApiForbiddenResponse(xhr)) { + rejectPromise(`Forbidden: ${response.message}`); + } else { + // A 403 should always be a forbidden response. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 403: ${xhr.responseText}` + ); + } + } else { + rejectPromise( + `API error: Unexpected HTTP status ${xhr.status}: ${error}` + ); + } + } catch (_e) { + rejectPromise(error); + } + }, + }, + JSON.stringify(requestBody), + { method: 'POST' } + ); + return promise; + } + } + + class UID2CstgBox { + static _namedCurve = 'P-256'; + constructor(clientPublicKey, sharedKey) { + this._clientPublicKey = clientPublicKey; + this._sharedKey = sharedKey; + } + + static async build(serverPublicKey) { + const clientKeyPair = await UID2CstgCrypto.generateKeyPair( + UID2CstgBox._namedCurve + ); + const importedServerPublicKey = await UID2CstgCrypto.importPublicKey( + serverPublicKey, + this._namedCurve + ); + const sharedKey = await UID2CstgCrypto.deriveKey( + importedServerPublicKey, + clientKeyPair.privateKey + ); + return new UID2CstgBox(clientKeyPair.publicKey, sharedKey); + } + + async encrypt(plaintext, additionalData) { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData, + }, + this._sharedKey, + plaintext + ); + return { iv, ciphertext }; + } + + async decrypt(iv, ciphertext) { + return window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + this._sharedKey, + ciphertext + ); + } + + get clientPublicKey() { + return this._clientPublicKey; + } + } + + class UID2CstgCrypto { + static base64ToBytes(base64) { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); + } + + static bytesToBase64(bytes) { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join( + '' + ); + return btoa(binString); + } + + static async generateKeyPair(namedCurve) { + const params = { + name: 'ECDH', + namedCurve: namedCurve, + }; + return window.crypto.subtle.generateKey(params, false, ['deriveKey']); + } + + static async importPublicKey(publicKey, namedCurve) { + const params = { + name: 'ECDH', + namedCurve: namedCurve, + }; + return window.crypto.subtle.importKey( + 'spki', + this.base64ToBytes(publicKey), + params, + false, + [] + ); + } + + static exportPublicKey(publicKey) { + return window.crypto.subtle.exportKey('spki', publicKey); + } + + static async deriveKey(serverPublicKey, clientPrivateKey) { + return window.crypto.subtle.deriveKey( + { + name: 'ECDH', + public: serverPublicKey, + }, + clientPrivateKey, + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'] + ); + } + + static async hash(value) { + const hash = await window.crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(value) + ); + return this.bytesToBase64(new Uint8Array(hash)); + } + } +} + export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { let suppliedToken = null; const preferLocalStorage = (config.storage !== 'cookie'); const storageManager = new Uid2StorageManager(prebidStorageManager, preferLocalStorage, config.internalStorage, _logInfo); _logInfo(`Module is using ${preferLocalStorage ? 'local storage' : 'cookies'} for internal storage.`); - if (config.paramToken) { + const isCstgEnabled = + clientSideTokenGenerator && + clientSideTokenGenerator.isCSTGOptionsValid(config.cstg, _logWarn); + if (isCstgEnabled) { + _logInfo(`Module is using client-side token generation.`); + // Ignores config.paramToken and config.serverCookieName if any is provided + suppliedToken = null; + } else if (config.paramToken) { suppliedToken = config.paramToken; _logInfo('Read token from params', suppliedToken); } else if (config.serverCookieName) { @@ -184,7 +686,7 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { if (storedTokens && typeof storedTokens === 'string') { // Stored value is a plain token - if no token is supplied, just use the stored value. - if (!suppliedToken) { + if (!suppliedToken && !isCstgEnabled) { _logInfo('Returning legacy cookie value.'); return { id: storedTokens }; } @@ -200,11 +702,31 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { storedTokens = null; } } - // At this point, any legacy values or superseded stored tokens have been nulled out. + + if (FEATURES.UID2_CSTG && isCstgEnabled) { + const cstgIdentity = clientSideTokenGenerator.getValidIdentity(config.cstg, _logWarn); + if (cstgIdentity) { + if (storedTokens && clientSideTokenGenerator.isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn)) { + storedTokens = null; + } + + if (!storedTokens || Date.now() > storedTokens.latestToken.refresh_expires) { + const promise = clientSideTokenGenerator.generateTokenAndStore(config.apiBaseUrl, config.cstg, cstgIdentity, storageManager, _logInfo, _logWarn); + _logInfo('Generate token using CSTG'); + return { callback: (cb) => { + promise.then((result) => { + _logInfo('Token generation responded, passing the new token on.', result); + cb(result); + }); + } }; + } + } + } + const useSuppliedToken = !(storedTokens?.latestToken) || (suppliedToken && suppliedToken.identity_expires > storedTokens.latestToken.identity_expires); const newestAvailableToken = useSuppliedToken ? suppliedToken : storedTokens.latestToken; _logInfo('UID2 module selected latest token', useSuppliedToken, newestAvailableToken); - if (!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires) { + if ((!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires)) { _logInfo('Newest available token is expired and not refreshable.'); return { id: null }; } @@ -227,6 +749,21 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { originalToken: suppliedToken ?? storedTokens?.originalToken, latestToken: newestAvailableToken, }; + if (FEATURES.UID2_CSTG && isCstgEnabled) { + tokens.originalIdentity = storedTokens?.originalIdentity; + } storageManager.storeValue(tokens); return { id: tokens }; } + +export function extractIdentityFromParams(params) { + const keysToCheck = ['emailHash', 'phoneHash', 'email', 'phone']; + + for (let key of keysToCheck) { + if (params.hasOwnProperty(key)) { + return { [key]: params[key] }; + } + } + + return {}; +} diff --git a/modules/underdogmediaBidAdapter.js b/modules/underdogmediaBidAdapter.js index 7561cdb60de..54b74c7ccd4 100644 --- a/modules/underdogmediaBidAdapter.js +++ b/modules/underdogmediaBidAdapter.js @@ -4,7 +4,6 @@ import { getWindowSelf, getWindowTop, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logMessage, logWarn, @@ -12,6 +11,7 @@ import { } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'underdogmedia'; const UDM_ADAPTER_VERSION = '7.30V'; @@ -71,7 +71,7 @@ export const spec = { let data = { dt: 10, gdpr: {}, - pbTimeout: config.getConfig('bidderTimeout'), + pbTimeout: +config.getConfig('bidderTimeout') || 3001, // KP: convert to number and if NaN we default to 3001. Particular value to let us know that there was a problem in converting pbTimeout pbjsVersion: prebidVersion, placements: [], ref: deepAccess(bidderRequest, 'refererInfo.page') ? bidderRequest.refererInfo.page : undefined, diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index 9dcbefd374b..c7e8102ffc9 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -139,6 +139,7 @@ export const spec = { domain: domain, placementId: bidReq.params.placementId != undefined ? bidReq.params.placementId : null, publisherId: bidReq.params.publisherId, + gpid: deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot', '')), sizes: bidReq.sizes, params: bidReq.params }; diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 66aaf4a17e5..429ba8f60ba 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -87,10 +87,33 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { accountId: deepAccess(validBidRequests[0], 'params.accountId') } }; + const eids = initializeEids(validBidRequests[0]); + if (eids.length > 0) { + request.user.eids = eids; + } + logInfo('[UNICORN] OpenRTB Formatted Request:', request); return JSON.stringify(request); } +const initializeEids = (bidRequest) => { + let eids = []; + + let id5 = deepAccess(bidRequest, 'userId.id5id.uid'); + if (id5) { + eids.push({ + source: 'id5-sync.com', + uids: [ + { + id: id5 + } + ] + }); + } + + return eids; +} + const interpretResponse = (serverResponse, request) => { logInfo('[UNICORN] interpretResponse.serverResponse:', serverResponse); logInfo('[UNICORN] interpretResponse.request:', request); diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index d893bfd3038..b825003f36f 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -56,6 +56,12 @@ const RemoveDuplicateSizes = (validBid) => { } }; +const ConfigureProtectedAudience = (validBid, protectedAudienceEnabled) => { + if (!protectedAudienceEnabled && validBid.ortb2Imp && validBid.ortb2Imp.ext) { + delete validBid.ortb2Imp.ext.ae; + } +} + const getRequests = (conf, validBidRequests, bidderRequest) => { const {bids, bidderRequestId, bidderCode, ...bidderRequestData} = bidderRequest; const invalidBidsCount = bidderRequest.bids.length - validBidRequests.length; @@ -65,6 +71,7 @@ const getRequests = (conf, validBidRequests, bidderRequest) => { const currSiteId = validBid.params.siteId; addBidFloorInfo(validBid); RemoveDuplicateSizes(validBid); + ConfigureProtectedAudience(validBid, conf.protectedAudienceEnabled); requestBySiteId[currSiteId] = requestBySiteId[currSiteId] || []; requestBySiteId[currSiteId].push(validBid); }); @@ -73,7 +80,14 @@ const getRequests = (conf, validBidRequests, bidderRequest) => { Object.keys(requestBySiteId).forEach((key) => { let data = { - bidderRequest: Object.assign({}, {bids: requestBySiteId[key], invalidBidsCount, ...bidderRequestData}) + bidderRequest: Object.assign({}, + { + bids: requestBySiteId[key], + invalidBidsCount, + prebidVersion: '$prebid.version$', + ...bidderRequestData + } + ) }; request.push(Object.assign({}, {data, ...conf})); @@ -206,21 +220,49 @@ export const adapter = { endPoint = deepAccess(validBidRequests[0], 'params.endpoint') || endPoint; } - const url = endPoint; - const method = 'POST'; - const options = {contentType: 'application/json'}; - return getRequests({url, method, options}, validBidRequests, bidderRequest); + return getRequests({ + 'url': endPoint, + 'method': 'POST', + 'options': { + 'contentType': 'application/json' + }, + 'protectedAudienceEnabled': bidderRequest.fledgeEnabled + }, validBidRequests, bidderRequest); }, - interpretResponse: function (serverResponse = {}) { + interpretResponse: function (serverResponse) { + if (!(serverResponse && serverResponse.body && (serverResponse.body.auctionConfigs || serverResponse.body.bids))) { + return []; + } + const serverResponseBody = serverResponse.body; + let bids = []; + let fledgeAuctionConfigs = null; + if (serverResponseBody.bids.length) { + bids = handleBidResponseByMediaType(serverResponseBody.bids); + } - const noBidsResponse = []; - const isInvalidResponse = !serverResponseBody || !serverResponseBody.bids; + if (serverResponseBody.auctionConfigs) { + let auctionConfigs = serverResponseBody.auctionConfigs; + let bidIdList = Object.keys(auctionConfigs); + if (bidIdList.length) { + bidIdList.forEach((bidId) => { + fledgeAuctionConfigs = [{ + 'bidId': bidId, + 'config': auctionConfigs[bidId] + }]; + }) + } + } - return isInvalidResponse - ? noBidsResponse - : handleBidResponseByMediaType(serverResponseBody.bids); + if (!fledgeAuctionConfigs) { + return bids; + } + + return { + bids, + fledgeAuctionConfigs + }; } }; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index dc362b41136..e5f7e3b8fb2 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -1,4 +1,4 @@ -import {deepAccess, isFn, isPlainObject, isStr} from '../../src/utils.js'; +import {deepAccess, deepClone, isFn, isPlainObject, isStr} from '../../src/utils.js'; export const EID_CONFIG = new Map(); @@ -32,34 +32,23 @@ function createEidObject(userIdData, subModuleKey) { return null; } -// this function will generate eids array for all available IDs in bidRequest.userId -// this function will be called by userId module -// if any adapter does not want any particular userId to be passed then adapter can use Array.filter(e => e.source != 'tdid') export function createEidsArray(bidRequestUserId) { - let eids = []; - - for (const subModuleKey in bidRequestUserId) { - if (bidRequestUserId.hasOwnProperty(subModuleKey)) { - if (subModuleKey === 'pubProvidedId') { - eids = eids.concat(bidRequestUserId['pubProvidedId']); - } else if (Array.isArray(bidRequestUserId[subModuleKey])) { - bidRequestUserId[subModuleKey].forEach((config, index, arr) => { - const eid = createEidObject(config, subModuleKey); - - if (eid) { - eids.push(eid); - } - }) - } else { - const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); - if (eid) { - eids.push(eid); - } - } + const allEids = {}; + function collect(eid) { + const key = JSON.stringify([eid.source?.toLowerCase(), eid.ext]); + if (allEids.hasOwnProperty(key)) { + allEids[key].uids.push(...eid.uids); + } else { + allEids[key] = eid; } } - return eids; + Object.entries(bidRequestUserId).forEach(([name, values]) => { + values = Array.isArray(values) ? values : [values]; + const eids = name === 'pubProvidedId' ? deepClone(values) : values.map(value => createEidObject(value, name)); + eids.filter(eid => eid != null).forEach(collect); + }) + return Object.values(allEids); } /** diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 04073923ed1..c8d27cc9d52 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -77,6 +77,7 @@ userIdAsEids = [ uids: [{ id: 'the-ids-object-stringified', atype: 1 + }] }, { @@ -117,6 +118,50 @@ userIdAsEids = [ }] }, + { + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'liveintent.sovrn.com'', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'openx.net'', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'pubmatic.com'', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + { source: 'media.net', uids: [{ @@ -279,6 +324,13 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 3 }] + }, + { + source: 'mygaru.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] } ] ``` diff --git a/modules/userId/index.js b/modules/userId/index.js index b20b38f0e40..5a088b27319 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -668,8 +668,8 @@ function encryptSignals(signals, version = 1) { } /** -* This function will be exposed in the global-name-space so that publisher can register the signals-ESP. -*/ + * This function will be exposed in the global-name-space so that publisher can register the signals-ESP. + */ function registerSignalSources() { if (!isGptPubadsDefined()) { return; @@ -886,14 +886,12 @@ function updateInitializedSubmodules(dest, submodule) { /** * list of submodule configurations with valid 'storage' or 'value' obj definitions - * * storage config: contains values for storing/retrieving User ID data in browser storage - * * value config: object properties that are copied to bids (without saving to storage) + * storage config: contains values for storing/retrieving User ID data in browser storage + * value config: object properties that are copied to bids (without saving to storage) * @param {SubmoduleConfig[]} configRegistry - * @param {Submodule[]} submoduleRegistry - * @param {string[]} activeStorageTypes * @returns {SubmoduleConfig[]} */ -function getValidSubmoduleConfigs(configRegistry, submoduleRegistry) { +function getValidSubmoduleConfigs(configRegistry) { if (!Array.isArray(configRegistry)) { return []; } @@ -958,7 +956,7 @@ function updateEIDConfig(submodules) { */ function updateSubmodules() { updateEIDConfig(submoduleRegistry); - const configs = getValidSubmoduleConfigs(configRegistry, submoduleRegistry); + const configs = getValidSubmoduleConfigs(configRegistry); if (!configs.length) { return; } diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 01930a67dda..7a01e128814 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -158,6 +158,9 @@ pbjs.setConfig({ }, { name: "gravitompId" + }, + { + name: "mygaruId" } ], syncDelay: 5000, diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index fa1e2898a4a..05960378d23 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,6 +1,6 @@ -import {getAdUnitSizes} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'vdoai'; const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; diff --git a/modules/ventesBidAdapter.js b/modules/ventesBidAdapter.js index 73ae0a7e5f1..78c580c4116 100644 --- a/modules/ventesBidAdapter.js +++ b/modules/ventesBidAdapter.js @@ -1,8 +1,9 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {convertCamelToUnderscore, isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; +import {isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://ad.ventesavenues.in/va/ad'; diff --git a/modules/viantOrtbBidAdapter.js b/modules/viantOrtbBidAdapter.js index e7bf9129a62..0f7953a192a 100644 --- a/modules/viantOrtbBidAdapter.js +++ b/modules/viantOrtbBidAdapter.js @@ -5,7 +5,7 @@ import {ortbConverter} from '../libraries/ortbConverter/converter.js' import {deepAccess, getBidIdParameter, logError} from '../src/utils.js'; const BIDDER_CODE = 'viant'; -const ENDPOINT = 'https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder_test' +const ENDPOINT = 'https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder' const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; diff --git a/modules/viantOrtbBidAdapter.md b/modules/viantOrtbBidAdapter.md index 178c2c18a0f..def93722b7b 100644 --- a/modules/viantOrtbBidAdapter.md +++ b/modules/viantOrtbBidAdapter.md @@ -2,7 +2,7 @@ Module Name: VIANT Bidder Adapter Module Type: Bidder Adapter -Maintainer: dist-runtime@viantinc.com +Maintainer: Marketplace@adelphic.com # Description diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index ba21a9c82d2..b5323181c6c 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -1,9 +1,10 @@ -import {_each, chunk, deepAccess, isFn, parseSizesInput, parseUrl, uniques, isArray} from '../src/utils.js'; +import {_each, deepAccess, isFn, parseSizesInput, parseUrl, uniques, isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {bidderSettings} from '../src/bidderSettings.js'; import {config} from '../src/config.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; diff --git a/modules/videoreachBidAdapter.js b/modules/videoreachBidAdapter.js index 9fd5853c75e..8835398d7cc 100644 --- a/modules/videoreachBidAdapter.js +++ b/modules/videoreachBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'videoreach'; const ENDPOINT_URL = 'https://a.videoreach.com/hb/'; diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index c1bb626a39c..a86c958392e 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,9 +1,11 @@ -import { triggerPixel, parseSizesInput, deepAccess, logError, getGptSlotInfoForAdUnitCode } 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 { INSTREAM as VIDEO_INSTREAM } from '../src/video.js'; +import {deepAccess, logError, parseSizesInput, triggerPixel} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM as VIDEO_INSTREAM} from '../src/video.js'; import {getStorageManager} from '../src/storageManager.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; + const BIDDER_CODE = 'visx'; const GVLID = 154; const BASE_URL = 'https://t.visx.net'; @@ -13,6 +15,7 @@ const TIME_TO_LIVE = 360; const DEFAULT_CUR = 'EUR'; const ADAPTER_SYNC_PATH = '/push_sync'; const TRACK_TIMEOUT_PATH = '/track/bid_timeout'; +const RUNTIME_STATUS_RESPONSE_TIME = 999000; const LOG_ERROR_MESS = { noAuid: 'Bid from response has no auid parameter - ', noAdm: 'Bid from response has no adm parameter - ', @@ -31,6 +34,7 @@ const LOG_ERROR_MESS = { }; const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN']; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const _bidResponseTimeLogged = []; export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -206,6 +210,12 @@ export const spec = { if (bid.ext && bid.ext.events && bid.ext.events.win) { triggerPixel(bid.ext.events.win); } + // Call 'track/runtime' with the corresponding bid.requestId - only once per auction + if (bid.ext && bid.ext.events && bid.ext.events.runtime && !_bidResponseTimeLogged.includes(bid.auctionId)) { + _bidResponseTimeLogged.push(bid.auctionId); + const _roundedTime = _roundResponseTime(bid.timeToRespond, 50); + triggerPixel(bid.ext.events.runtime.replace('{STATUS_CODE}', RUNTIME_STATUS_RESPONSE_TIME + _roundedTime)); + } }, onTimeout: function(timeoutData) { // Call '/track/bid_timeout' with timeout data @@ -319,6 +329,10 @@ function _addBidResponse(serverBid, bidsMap, currency, bidResponses) { if (serverBid.ext && serverBid.ext.prebid) { bidResponse.ext = serverBid.ext.prebid; + if (serverBid.ext.visx && serverBid.ext.visx.events) { + const prebidExtEvents = bidResponse.ext.events || {}; + bidResponse.ext.events = Object.assign(prebidExtEvents, serverBid.ext.visx.events); + } } const visxTargeting = deepAccess(serverBid, 'ext.prebid.targeting'); @@ -432,4 +446,15 @@ function _getUserId() { return null; } +function _roundResponseTime(time, timeRange) { + if (time <= 0) { + return 0; // Special code for scriptLoadTime of 0 ms or less + } else if (time > 5000) { + return 100; // Constant code for scriptLoadTime greater than 5000 ms + } else { + const roundedValue = Math.floor((time - 1) / timeRange) + 1; + return roundedValue; + } +} + registerBidder(spec); diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js index 34bd46ccb98..431d0887334 100644 --- a/modules/voxBidAdapter.js +++ b/modules/voxBidAdapter.js @@ -12,6 +12,7 @@ const BIDDER_CODE = 'vox'; const SSP_ENDPOINT = 'https://ssp.hybrid.ai/auction/prebid'; const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const TTL = 60; +const GVLID = 206; function buildBidRequests(validBidRequests) { return _map(validBidRequests, function(bid) { @@ -183,6 +184,7 @@ function wrapBanner(bid, bidData) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], /** diff --git a/modules/vrtcalBidAdapter.js b/modules/vrtcalBidAdapter.js index 21870e3218a..07dc35525c3 100644 --- a/modules/vrtcalBidAdapter.js +++ b/modules/vrtcalBidAdapter.js @@ -5,6 +5,8 @@ import { config } from '../src/config.js'; import {deepAccess, isFn, isPlainObject} from '../src/utils.js'; const GVLID = 706; +const VRTCAL_USER_SYNC_URL_IFRAME = `https://usync.vrtcal.com/i?ssp=1804&synctype=iframe`; +const VRTCAL_USER_SYNC_URL_REDIRECT = `https://usync.vrtcal.com/i?ssp=1804&synctype=redirect`; export const spec = { code: 'vrtcal', @@ -67,7 +69,7 @@ export const spec = { name: 'VRTCAL_FILLED', cat: deepAccess(bid, 'ortb2.site.cat', []), domain: decodeURIComponent(window.location.href).replace('https://', '').replace('http://', '').split('/')[0], - page: bid.refererInfo.page + page: window.location.href }, device: { language: navigator.language, @@ -148,7 +150,34 @@ export const spec = { ); ajax(winUrl, null); return true; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { + const syncs = []; + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `&us_privacy=${encodeURIComponent(uspConsent)}`; + const gpp = gppConsent.gppString ? gppConsent.gppString : ''; + const gppSid = Array.isArray(gppConsent.applicableSections) ? gppConsent.applicableSections.join(',') : ''; + let vrtcalSyncURL = '' + + if (syncOptions.iframeEnabled) { + vrtcalSyncURL = `${VRTCAL_USER_SYNC_URL_IFRAME}${usPrivacy}${gdprFlag}${gdprString}&gpp=${gpp}&gpp_sid=${gppSid}&surl=`; + syncs.push({ + type: 'iframe', + url: vrtcalSyncURL + }); + } else { + vrtcalSyncURL = `${VRTCAL_USER_SYNC_URL_REDIRECT}${usPrivacy}${gdprFlag}${gdprString}&gpp=${gpp}&gpp_sid=${gppSid}&surl=`; + syncs.push({ + type: 'image', + url: vrtcalSyncURL + }); + } + + return syncs; } + }; registerBidder(spec); diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 6ba502d2c8b..a0963b844e1 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -7,25 +7,29 @@ * @requires module:modules/realTimeData */ -/** profile metadata +/** + * profile metadata * @typedef dataCallbackMetadata * @property {boolean} user if true it is user-centric data * @property {string} source describe the source of data, if 'contextual' or 'wam' * @property {boolean} isDefault if true it the default profile defined in the configuration */ -/** profile from contextual, wam or sfbx +/** + * profile from contextual, wam or sfbx * @typedef {Object.} Profile */ -/** onData callback type +/** + * onData callback type * @callback dataCallback * @param {Profile} data profile data * @param {dataCallbackMetadata} meta metadata * @returns {void} */ -/** setPrebidTargeting callback type +/** + * setPrebidTargeting callback type * @callback setPrebidTargetingCallback * @param {string} adUnitCode * @param {Profile} data @@ -33,7 +37,8 @@ * @returns {boolean} */ -/** sendToBidders callback type +/** + * sendToBidders callback type * @callback sendToBiddersCallback * @param {Object} bid * @param {string} adUnitCode @@ -90,7 +95,8 @@ * @property {?boolean} enabled if false, will ignore this configuration */ -/** common configuration between contextual, wam and sfbx +/** + * common configuration between contextual, wam and sfbx * @typedef {WeboCtxConf|WeboUserDataConf|SfbxLiteDataConf} CommonConf */ @@ -109,8 +115,7 @@ import { isBoolean, isPlainObject, logWarn, - mergeDeep, - tryAppendQueryString + mergeDeep } from '../src/utils.js'; import { submodule @@ -123,6 +128,7 @@ import { } from '../src/storageManager.js'; import adapterManager from '../src/adapterManager.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -188,7 +194,8 @@ class WeboramaRtdProvider { constructor(components) { this.#components = components; } - /** Initialize module + /** + * Initialize module * @method * @param {Object} moduleConfig * @param {?ModuleParams} moduleConfig.params @@ -219,7 +226,8 @@ class WeboramaRtdProvider { return Object.values(this.#components).some((c) => c.initialized); } - /** function that will allow RTD sub-modules to modify the AdUnit object for each auction + /** + * function that will allow RTD sub-modules to modify the AdUnit object for each auction * @method * @param {Object} reqBidsConfigObj * @param {doneCallback} onDone @@ -253,7 +261,8 @@ class WeboramaRtdProvider { }); } - /** function that provides ad server targeting data to RTD-core + /** + * function that provides ad server targeting data to RTD-core * @method * @param {string[]} adUnitsCodes * @param {Object} moduleConfig @@ -294,7 +303,8 @@ class WeboramaRtdProvider { } } - /** Initialize subsection module + /** + * Initialize subsection module * @method * @private * @param {ModuleParams} moduleParams @@ -330,7 +340,8 @@ class WeboramaRtdProvider { return true; } - /** normalize submodule configuration + /** + * normalize submodule configuration * @method * @private * @param {ModuleParams} moduleParams @@ -363,7 +374,8 @@ class WeboramaRtdProvider { } } - /** coerce setPrebidTargeting to a callback + /** + * coerce setPrebidTargeting to a callback * @method * @private * @param {CommonConf} submoduleParams @@ -379,7 +391,8 @@ class WeboramaRtdProvider { } } - /** coerce sendToBidders to a callback + /** + * coerce sendToBidders to a callback * @method * @private * @param {CommonConf} submoduleParams @@ -426,7 +439,8 @@ class WeboramaRtdProvider { * @typedef {Object} AdUnit * @property {Object[]} bids */ - /** function that handles bid request data + /** + * function that handles bid request data * @method * @private * @param {Object} reqBidsConfigObj @@ -476,18 +490,21 @@ class WeboramaRtdProvider { }); } - /** onSuccess callback type + /** + * onSuccess callback type * @callback successCallback * @param {?Object} data * @returns {void} */ - /** onDone callback type + /** + * onDone callback type * @callback doneCallback * @returns {void} */ - /** Fetch Bigsea Contextual Profile + /** + * Fetch Bigsea Contextual Profile * @method * @private * @param {WeboCtxConf} weboCtxConf @@ -566,7 +583,8 @@ class WeboramaRtdProvider { ajax(urlProfileAPI, callback, null, options); } - /** set bigsea contextual profile on module state + /** + * set bigsea contextual profile on module state * @method * @private * @param {?Object} data @@ -579,7 +597,8 @@ class WeboramaRtdProvider { } } - /** function that provides data handlers based on the configuration + /** + * function that provides data handlers based on the configuration * @method * @private * @param {ModuleParams} moduleParams @@ -667,7 +686,8 @@ class WeboramaRtdProvider { onData: dataConf.onData, }; } - /** handle individual bid + /** + * handle individual bid * @method * @private * @param {Object} reqBidsConfigObj @@ -694,7 +714,8 @@ class WeboramaRtdProvider { } } - /** function that handles bid request data + /** + * function that handles bid request data * @method * @private * @param {ProfileHandler} ph profile handler @@ -705,7 +726,8 @@ class WeboramaRtdProvider { return [deepClone(ph.data), deepClone(ph.metadata)]; } - /** handle appnexus/xandr bid + /** + * handle appnexus/xandr bid * @method * @private * @param {Object} reqBidsConfigObj @@ -723,7 +745,8 @@ class WeboramaRtdProvider { // this.#setBidderOrtb2(reqBidsConfigObj.ortb2Fragments?.bidder, bid.bidder, base, profile); } - /** handle generic bid via ortb2 arbitrary data + /** + * handle generic bid via ortb2 arbitrary data * @method * @private * @param {Object} reqBidsConfigObj @@ -844,7 +867,8 @@ export function isValidProfile(profile) { * @returns {buildProfileHandlerCallback} */ function getContextualProfile(component /* equivalent to this */) { - /** return contextual profile + /** + * return contextual profile * @param {WeboCtxConf} weboCtxConf * @returns {[Profile,boolean]} contextual profile + isDefault boolean flag */ @@ -865,7 +889,8 @@ function getContextualProfile(component /* equivalent to this */) { * @returns {buildProfileHandlerCallback} */ function getWeboUserDataProfile(component /* equivalent to this */) { - /** return weboUserData profile + /** + * return weboUserData profile * @param {WeboUserDataConf} weboUserDataConf * @returns {[Profile,boolean]} weboUserData profile + isDefault boolean flag */ @@ -885,7 +910,8 @@ function getWeboUserDataProfile(component /* equivalent to this */) { * @returns {buildProfileHandlerCallback} */ function getSfbxLiteDataProfile(component /* equivalent to this */) { - /** return weboUserData profile + /** + * return weboUserData profile * @param {SfbxLiteDataConf} sfbxLiteDataConf * @returns {[Profile,boolean]} sfbxLiteData profile + isDefault boolean flag */ @@ -909,7 +935,8 @@ function getSfbxLiteDataProfile(component /* equivalent to this */) { * @returns {void} */ -/** return generic webo data profile +/** + * return generic webo data profile * @param {WeboUserDataConf|SfbxLiteDataConf} weboDataConf * @param {cacheGetCallback} cacheGet * @param {cacheSetCallback} cacheSet diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 5cc063d16b4..41efc432e11 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -1,6 +1,4 @@ import { - convertCamelToUnderscore, - convertTypes, deepAccess, getBidRequest, getParameterByName, @@ -16,7 +14,9 @@ import {BANNER} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'winr'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; diff --git a/modules/xeBidAdapter.js b/modules/xeBidAdapter.js index 94a66e9980e..bff76aae172 100644 --- a/modules/xeBidAdapter.js +++ b/modules/xeBidAdapter.js @@ -1,7 +1,8 @@ import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getAdUnitSizes, parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray } from '../src/utils.js'; +import {parseSizesInput, isFn, deepAccess, logError, isArray, getBidIdParameter} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const CUR = 'USD'; const BIDDER_CODE = 'xe'; @@ -141,12 +142,12 @@ function interpretResponse(serverResponse, { bidderRequest }) { } /** -* Register the user sync pixels which should be dropped after the auction. -* -* @param {SyncOptions} syncOptions Which user syncs are allowed? -* @param {ServerResponse[]} serverResponses List of server's responses. -* @return {UserSync[]} The user syncs which should be dropped. -*/ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { const syncs = []; const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); @@ -171,11 +172,11 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent } /** -* Get valid floor value from getFloor fuction. -* -* @param {Object} bid Current bid request. -* @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. -*/ + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ export function getBidFloor(bid) { if (!isFn(bid.getFloor)) { return null; diff --git a/modules/yahoosspBidAdapter.js b/modules/yahoosspBidAdapter.js index a66d76f8689..0453350a85a 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahoosspBidAdapter.js @@ -111,7 +111,7 @@ function extractUserSyncUrls(syncOptions, pixels) { * @param {object} consentData * @param {object} consentData.gpp * @param {string} consentData.gpp.gppConsent - * @param {array} consentData.gpp.applicableSections + * @param {Array} consentData.gpp.applicableSections * @param {object} consentData.gdpr * @param {object} consentData.gdpr.consentString * @param {object} consentData.gdpr.gdprApplies diff --git a/modules/yahoosspBidAdapter.md b/modules/yahoosspBidAdapter.md index c8c42930e5b..62fe0f22a55 100644 --- a/modules/yahoosspBidAdapter.md +++ b/modules/yahoosspBidAdapter.md @@ -1,7 +1,7 @@ # Overview **Module Name:** Yahoo Advertising Bid Adapter **Module Type:** Bidder Adapter -**Maintainer:** hb-fe-tech@yahooinc.com +**Maintainer:** prebid-tech-team@yahooinc.com # Description The Yahoo Advertising Bid Adapter is an OpenRTB interface that consolidates all previous "Oath.inc" adapters such as: "aol", "oneMobile", "oneDisplay" & "oneVideo" supply-side platforms. @@ -19,6 +19,16 @@ The Yahoo Advertising Bid Adapter is an OpenRTB interface that consolidates all * First Party Data (ortb2 & ortb2Imp) * Custom TTL (time to live) +# Adapter Aliases +Whilst the primary bidder code for this bid adapter is `yahooAds`, the aliases `yahoossp` and `yahooAdvertising` can be used to enable this adapter. If you wish to set Prebid configuration specifically for this bid adapter, then the configuration key _must_ match the used bidder code. All examples in this documentation use the primiry bidder code, but switching `yahooAds` with one of the relevant aliases may be required for your setup. Let's take [setting the request mode](#adapter-request-mode) as an example; if you used the `yahoossp` alias, then the corresponding `setConfig` API call would look like this: + +```javascript +pbjs.setConfig({ + yahoossp: { + mode: 'banner' // 'all', 'video', 'banner' (default) + } +}); +``` # Adapter Request mode Since the Yahoo Advertising bid adapter supports both Banner and Video adUnits, a controller was needed to allow you to define when the adapter should generate a bid-requests to the Yahoo bid endpoint. diff --git a/modules/yandexBidAdapter.js b/modules/yandexBidAdapter.js index 9ca989b2259..cb1cbd89c84 100644 --- a/modules/yandexBidAdapter.js +++ b/modules/yandexBidAdapter.js @@ -4,10 +4,55 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js' import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @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 + * @typedef {import('../src/auction.js').BidderRequest} BidderRequest + * @typedef {import('../src/mediaTypes.js').MediaType} MediaType + * @typedef {import('../src/utils.js').MediaTypes} MediaTypes + * @typedef {import('../modules/priceFloors.js').getFloor} GetFloor + */ + +/** + * @typedef {Object} CustomServerRequestFields + * @property {BidRequest} bidRequest + */ + +/** + * @typedef {ServerRequest & CustomServerRequestFields} YandexServerRequest + */ + +/** + * Yandex bidder-specific params which the publisher used in their bid request. + * + * @typedef {Object} YandexBidRequestParams + * @property {string} placementId Possible formats: `R-I-123456-2`, `R-123456-1`, `123456-789`. + * @property {number} [pageId] Deprecated. Please use `placementId` instead. + * @property {number} [impId] Deprecated. Please use `placementId` instead. + */ + +/** + * @typedef {Object} AdditionalBidRequestFields + * @property {GetFloor} [getFloor] + * @property {MediaTypes} [mediaTypes] + */ + +/** + * @typedef {BidRequest & AdditionalBidRequestFields} ExtendedBidRequest + */ + const BIDDER_CODE = 'yandex'; const BIDDER_URL = 'https://bs.yandex.ru/prebid'; const DEFAULT_TTL = 180; const DEFAULT_CURRENCY = 'EUR'; +/** + * @type {MediaType[]} + */ const SUPPORTED_MEDIA_TYPES = [ BANNER, NATIVE ]; const SSP_ID = 10500; @@ -42,11 +87,18 @@ export const NATIVE_ASSETS = { const NATIVE_ASSETS_IDS = {}; _each(NATIVE_ASSETS, (asset, key) => { NATIVE_ASSETS_IDS[asset[0]] = key }); +/** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, aliases: ['ya'], // short code supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid request to validate. + * @returns {boolean} True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { const { params } = bid; if (!params) { @@ -59,6 +111,13 @@ export const spec = { return true; }, + /** + * Make a server request from the list of BidRequests. + * + * @param {ExtendedBidRequest[]} validBidRequests An array of bids. + * @param {BidderRequest} bidderRequest Bidder request object. + * @returns {YandexServerRequest[]} Objects describing the requests to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -131,6 +190,11 @@ export const spec = { deepSetValue(data, 'user.ext.eids', eids); } + const userData = deepAccess(bidRequest, 'ortb2.user.data'); + if (userData && userData.length) { + deepSetValue(data, 'user.data', userData); + } + const queryParamsString = formatQS(queryParams); return { method: 'POST', @@ -146,8 +210,12 @@ export const spec = { interpretResponse: interpretResponse, - onBidWon: function (bid) { - const nurl = bid['nurl']; + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction. + * @param {Bid} bid The bid that won the auction. + */ + onBidWon: function(bid) { + const nurl = addRTT(bid['nurl'], bid.timeToRespond); if (!nurl) { return; @@ -157,6 +225,9 @@ export const spec = { } } +/** + * @param {YandexBidRequestParams} bidRequestParams + */ function extractPlacementIds(bidRequestParams) { const { placementId } = bidRequestParams; const result = { pageId: null, impId: null }; @@ -191,6 +262,9 @@ function extractPlacementIds(bidRequestParams) { return result; } +/** + * @param {ExtendedBidRequest} bidRequest + */ function getBidfloor(bidRequest) { const floors = []; @@ -210,6 +284,9 @@ function getBidfloor(bidRequest) { return floors.sort((a, b) => b.floor - a.floor)[0]; } +/** + * @param {ExtendedBidRequest} bidRequest + */ function mapBanner(bidRequest) { if (deepAccess(bidRequest, 'mediaTypes.banner')) { const sizes = bidRequest.sizes || bidRequest.mediaTypes.banner.sizes; @@ -227,6 +304,9 @@ function mapBanner(bidRequest) { } } +/** + * @param {ExtendedBidRequest} bidRequest + */ function mapNative(bidRequest) { const adUnitNativeAssets = deepAccess(bidRequest, 'mediaTypes.native'); if (adUnitNativeAssets) { @@ -299,6 +379,13 @@ function mapImageAsset(adUnitImageAssetParams, nativeAssetType) { return img; } +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {YandexServerRequest} yandexServerRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ function interpretResponse(serverResponse, { bidRequest }) { let response = serverResponse.body; if (!response.seatbid) { @@ -313,6 +400,7 @@ function interpretResponse(serverResponse, { bidRequest }) { return bidsReceived.map(bidReceived => { const price = bidReceived.price; + /** @type {Bid} */ let prBid = { requestId: bidRequest.bidId, cpm: price, @@ -385,4 +473,24 @@ function replaceAuctionPrice(url, price, currency) { .replace(/\${AUCTION_CURRENCY}/, currency); } +function addRTT(url, rtt) { + if (!url) return; + + if (url.indexOf(`\${RTT}`) > -1) { + return url.replace(/\${RTT}/, rtt ?? -1); + } + + const urlObj = new URL(url); + + if (Number.isInteger(rtt)) { + urlObj.searchParams.set('rtt', rtt); + } else { + urlObj.searchParams.delete('rtt'); + } + + url = urlObj.toString(); + + return url; +} + registerBidder(spec); diff --git a/modules/yieldloveBidAdapter.js b/modules/yieldloveBidAdapter.js new file mode 100644 index 00000000000..4568206b20a --- /dev/null +++ b/modules/yieldloveBidAdapter.js @@ -0,0 +1,149 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +const DEFAULT_BID_TTL = 300; /* 5 minutes */ +const DEFAULT_CURRENCY = 'EUR'; + +const participatedBidders = [] + +export const spec = { + gvlid: 251, + code: 'yieldlove', + aliases: [], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(bid.params.pid && bid.params.rid) + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const anyValidBidRequest = validBidRequests[0] + + const impressions = validBidRequests.map(bidRequest => { + return { + ext: { + prebid: { + storedrequest: { + id: bidRequest.params.pid?.toString() + } + } + }, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + secure: 1, + id: bidRequest.bidId + } + }) + + const s2sRequest = { + device: { + ua: window.navigator.userAgent, + w: window.innerWidth, + h: window.innerHeight, + }, + site: { + ver: '1.9.0', + publisher: { + id: anyValidBidRequest.params.rid + }, + page: window.location.href, + domain: anyValidBidRequest.params.rid + }, + ext: { + prebid: { + targeting: {}, + cache: { + bids: {} + }, + storedrequest: { + id: anyValidBidRequest.params.rid + }, + } + }, + user: { + ext: { + consent: bidderRequest.gdprConsent?.consentString + }, + }, + id: utils.generateUUID(), + imp: impressions, + regs: { + ext: { + gdpr: 1 + } + } + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: s2sRequest, + options: { + contentType: 'text/plain', + withCredentials: true + }, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = [] + const seatBids = serverResponse.body?.seatbid || [] + seatBids.reduce((bids, cur) => { + if (cur.bid && cur.bid.length > 0) bids = bids.concat(cur.bid) + return bids + }, []).forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: true, + currency: DEFAULT_CURRENCY + }) + }) + + const bidders = serverResponse.body?.ext.responsetimemillis || {} + Object.keys(bidders).forEach(bidder => { + if (!participatedBidders.includes(bidder)) participatedBidders.push(bidder) + }) + + if (bidResponses.length === 0) { + utils.logInfo('interpretResponse :: no bid'); + } + + return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + let gdprParams = '' + gdprParams = `gdpr=${Number(gdprConsent?.gdprApplies)}&` + gdprParams += `gdpr_consent=${gdprConsent?.consentString || ''}` + + let bidderParams = '' + if (participatedBidders.length > 0) { + bidderParams = `bidders=${participatedBidders.join(',')}` + } + + syncs.push({ + type: 'iframe', + url: `https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&${gdprParams}&${bidderParams}` + }) + + return syncs + }, + +}; + +registerBidder(spec); diff --git a/modules/yieldloveBidAdapter.md b/modules/yieldloveBidAdapter.md new file mode 100644 index 00000000000..8fd71b55f88 --- /dev/null +++ b/modules/yieldloveBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Yieldlove Bid Adapter +Module Type: Bidder Adapter +Maintainer: adapter@yieldlove.com +``` + + +# Description + +Connects to **[Yieldlove](https://www.yieldlove.com/)**s S2S platform for bids. + +```js +const adUnits = [ + { + code: 'test-div', + mediaTypes: { banner: { sizes: [[300, 250]] }}, + bids: [ + { + bidder: 'yieldlove', + params: { + pid: 34437, + rid: 'website.com' + } + } + ] + } +] +``` + + +# Bid Parameters + +| Name | Scope | Description | Example | Type | +|---------------|--------------|---------------------------------------------------------|----------------------------|--------------| +| rid | **required** | Publisher ID on the Yieldlove platform | `website.com` | String | +| pid | **required** | Placement ID on the Yieldlove platform | `34437` | Number | diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 0e2c7b4bf59..9109c6e2a80 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -29,7 +29,7 @@ const VIDEO_PATH = '/exchange/prebidvideo'; const STAGE_DOMAIN = 'https://ads-stg.yieldmo.com'; const PROD_DOMAIN = 'https://ads.yieldmo.com'; const OUTSTREAM_VIDEO_PLAYER_URL = 'https://prebid-outstream.yieldmo.com/bundle.js'; -const OPENRTB_VIDEO_BIDPARAMS = ['mimes', 'startdelay', 'placement', 'startdelay', 'skipafter', 'protocols', 'api', +const OPENRTB_VIDEO_BIDPARAMS = ['mimes', 'startdelay', 'placement', 'plcmt', 'skipafter', 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos', 'skip', 'skippable']; const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const LOCAL_WINDOW = getWindowTop(); @@ -78,14 +78,17 @@ export const spec = { bust: new Date().getTime().toString(), dnt: getDNT(), description: getPageDescription(), + tmax: bidderRequest.timeout || 400, userConsent: JSON.stringify({ // case of undefined, stringify will remove param - gdprApplies: deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + gdprApplies: + deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', cmp: deepAccess(bidderRequest, 'gdprConsent.consentString') || '', gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || '', - gpp_sid: deepAccess(bidderRequest, 'gppConsent.applicableSections') || [] + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || [], }), - us_privacy: deepAccess(bidderRequest, 'uspConsent') || '' + us_privacy: deepAccess(bidderRequest, 'uspConsent') || '', }; if (canAccessTopWindow()) { @@ -432,12 +435,12 @@ function openRtbImpression(bidRequest) { } }; - const mediaTypesParams = deepAccess(bidRequest, 'mediaTypes.video'); + const mediaTypesParams = deepAccess(bidRequest, 'mediaTypes.video', {}); Object.keys(mediaTypesParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = mediaTypesParams[param]); - const videoParams = deepAccess(bidRequest, 'params.video'); + const videoParams = deepAccess(bidRequest, 'params.video', {}); Object.keys(videoParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = videoParams[param]); @@ -446,7 +449,7 @@ function openRtbImpression(bidRequest) { imp.video.skip = 1; delete imp.video.skippable; } - if (imp.video.placement !== 1) { + if (imp.video.plcmt !== 1 || imp.video.placement !== 1) { imp.video.startdelay = DEFAULT_START_DELAY; imp.video.playbackmethod = [ DEFAULT_PLAYBACK_METHOD ]; } diff --git a/modules/zetaBidAdapter.js b/modules/zetaBidAdapter.js index 527030efc9a..9a58e391a17 100644 --- a/modules/zetaBidAdapter.js +++ b/modules/zetaBidAdapter.js @@ -15,11 +15,11 @@ export const spec = { supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { // check for all required bid fields if (!(bid && @@ -50,12 +50,12 @@ export const spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {Bids[]} validBidRequests - an array of bidRequest objects - * @param {BidderRequest} bidderRequest - master bidRequest object - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const secure = 1; // treat all requests as secure const request = validBidRequests[0]; @@ -117,12 +117,12 @@ export const spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @param bidRequest The payload from the server's response. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { let bidResponse = []; if (Object.keys(serverResponse.body).length !== 0) { diff --git a/modules/zeta_global_sspAnalyticsAdapter.js b/modules/zeta_global_sspAnalyticsAdapter.js index ee3bb9cd5d6..751b55ec673 100644 --- a/modules/zeta_global_sspAnalyticsAdapter.js +++ b/modules/zeta_global_sspAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, logError } from '../src/utils.js'; +import {logInfo, logError} from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -10,6 +10,10 @@ const ADAPTER_CODE = 'zeta_global_ssp'; const BASE_URL = 'https://ssp.disqus.com/prebid/event'; const LOG_PREFIX = 'ZetaGlobalSsp-Analytics: '; +const cache = { + auctions: {} +}; + /// /////////// VARIABLES //////////////////////////////////// let publisherId; // int @@ -24,20 +28,87 @@ function sendEvent(eventType, event) { ); } +function getZetaParams(event) { + if (event.adUnits) { + for (const i in event.adUnits) { + const unit = event.adUnits[i]; + if (unit.bids) { + for (const j in unit.bids) { + const bid = unit.bids[j]; + if (bid.bidder === ADAPTER_CODE && bid.params) { + return bid.params; + } + } + } + } + } + return null; +} + /// /////////// ADAPTER EVENT HANDLER FUNCTIONS ////////////// function adRenderSucceededHandler(args) { let eventType = CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - sendEvent(eventType, args); + const event = { + adId: args.adId, + bid: { + adId: args.bid?.adId, + auctionId: args.bid?.auctionId, + adUnitCode: args.bid?.adUnitCode, + bidId: args.bid?.bidId, + requestId: args.bid?.requestId, + bidderCode: args.bid?.bidderCode, + mediaTypes: args.bid?.mediaTypes, + sizes: args.bid?.sizes, + adserverTargeting: args.bid?.adserverTargeting, + cpm: args.bid?.cpm, + creativeId: args.bid?.creativeId, + mediaType: args.bid?.mediaType, + renderer: args.bid?.renderer, + size: args.bid?.size, + timeToRespond: args.bid?.timeToRespond, + params: args.bid?.params + }, + doc: { + location: args.doc?.location + } + } + + // set zetaParams from cache + if (event.bid && event.bid.auctionId) { + const zetaParams = cache.auctions[event.bid.auctionId]; + if (zetaParams) { + event.bid.params = [ zetaParams ]; + } + } + + sendEvent(eventType, event); } function auctionEndHandler(args) { let eventType = CONSTANTS.EVENTS.AUCTION_END; logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - sendEvent(eventType, args); + const event = { + adUnitCodes: args.adUnitCodes, + adUnits: args.adUnits, + auctionEnd: args.auctionEnd, + auctionId: args.auctionId, + bidderRequests: args.bidderRequests, + bidsReceived: args.bidsReceived, + noBids: args.noBids, + winningBids: args.winningBids + } + + // save zetaParams to cache + const zetaParams = getZetaParams(event); + if (zetaParams && event.auctionId) { + cache.auctions[event.auctionId] = zetaParams; + } + + sendEvent(eventType, event); } /// /////////// ADAPTER DEFINITION /////////////////////////// diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index 687afb6c692..0c5db541286 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -45,7 +45,7 @@ export const spec = { supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. + * Determines whether the given bid request is valid. * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. @@ -54,7 +54,8 @@ export const spec = { // check for all required bid fields if (!(bid && bid.bidId && - bid.params)) { + bid.params && + bid.params.sid)) { logWarn('Invalid bid request - missing required bid data'); return false; } @@ -162,7 +163,8 @@ export const spec = { } provideEids(validBidRequests[0], payload); - const url = params.shortname ? ENDPOINT_URL.concat('?shortname=', params.shortname) : ENDPOINT_URL; + provideSegments(bidderRequest, payload); + const url = params.sid ? ENDPOINT_URL.concat('?sid=', params.sid) : ENDPOINT_URL; return { method: 'POST', url: url, @@ -334,6 +336,25 @@ function provideEids(request, payload) { } } +function provideSegments(bidderRequest, payload) { + const data = bidderRequest.ortb2?.user?.data; + if (isArray(data)) { + const segments = data.filter(d => d?.segment).map(d => d.segment).filter(s => isArray(s)).flatMap(s => s).filter(s => s?.id); + if (segments.length > 0) { + if (!payload.user) { + payload.user = {}; + } + if (!isArray(payload.user.data)) { + payload.user.data = []; + } + const payloadData = { + segment: segments + }; + payload.user.data.push(payloadData); + } + } +} + function provideMediaType(zetaBid, bid, bidRequest) { if (zetaBid.ext && zetaBid.ext.prebid && zetaBid.ext.prebid.type) { bid.mediaType = zetaBid.ext.prebid.type === VIDEO ? VIDEO : BANNER; diff --git a/package-lock.json b/package-lock.json index 04e92c2f79e..bac5bc0c929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "8.12.0-pre", + "version": "8.33.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "8.8.0-pre", + "version": "8.32.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.16.7", @@ -16,13 +16,13 @@ "core-js": "^3.13.0", "core-js-pure": "^3.13.0", "criteo-direct-rsa-validate": "^1.1.0", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dlv": "1.1.3", "dset": "3.1.2", "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^6.0.0" + "live-connect-js": "^6.3.4" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", @@ -47,6 +47,7 @@ "eslint": "^7.27.0", "eslint-config-standard": "^10.2.1", "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jsdoc": "^38.1.6", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prebid": "file:./plugins/eslint", "eslint-plugin-promise": "^5.1.0", @@ -90,6 +91,7 @@ "lodash": "^4.17.21", "mocha": "^10.0.0", "morgan": "^1.10.0", + "node-html-parser": "^6.1.5", "opn": "^5.4.0", "resolve-from": "^5.0.0", "sinon": "^4.1.3", @@ -127,11 +129,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -193,12 +196,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -310,9 +314,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -329,23 +333,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -465,28 +469,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -527,12 +531,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -540,9 +544,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1612,31 +1616,31 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1645,12 +1649,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1666,6 +1670,20 @@ "node": ">=0.1.90" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", + "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "dev": true, + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~2.2.5" + }, + "engines": { + "node": "^12 || ^14 || ^16 || ^17" + } + }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -6489,6 +6507,12 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7480,6 +7504,15 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -7838,9 +7871,9 @@ } }, "node_modules/crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/css": { "version": "3.0.0", @@ -7853,6 +7886,22 @@ "source-map-resolve": "^0.6.0" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-shorthand-properties": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", @@ -7865,6 +7914,18 @@ "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", "dev": true }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8691,12 +8752,67 @@ "void-elements": "^2.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "dev": true }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dset": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", @@ -8957,6 +9073,18 @@ "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -9458,6 +9586,55 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/eslint-plugin-jsdoc": { + "version": "38.1.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", + "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.22.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "regextras": "^0.8.0", + "semver": "^7.3.5", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": "^12 || ^14 || ^16 || ^17" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -10832,9 +11009,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -15469,14 +15646,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "engines": { - "node": ">=14" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15514,6 +15683,15 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", + "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -16240,32 +16418,19 @@ "dev": true }, "node_modules/live-connect-common": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.0.tgz", - "integrity": "sha512-pa1SuzCg8ovsB6OziAQZpDid/OT8k37VgWFQkE8OUmG52Kf9PUtJM8wqaGdMXd/rNAe/NH8m+Kxx9MZuOvn5zg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", + "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==", "engines": { "node": ">=18" } }, - "node_modules/live-connect-handlers": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/live-connect-handlers/-/live-connect-handlers-2.1.0.tgz", - "integrity": "sha512-uABe9D6yRp7HRgO6vhdIM5j88l17/ROzYGIOHc2Rv1TacLFH6IJ8sbmunY5mIJ9L6ArOVmL4WHY+QgOIkabhxg==", - "dependencies": { - "js-cookie": "^3.0.5", - "live-connect-common": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/live-connect-js": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.0.0.tgz", - "integrity": "sha512-4iMSeDPpueYoGEK4U152yKg+hj7jTxkzyfJUAGxtVHxdgjsjh8QmP3kLlTLBBrR/g/L/WCX47PBQxtGW9z8Rlw==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", + "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", "dependencies": { - "live-connect-common": "^3.0.0", - "live-connect-handlers": "^2.0.0", + "live-connect-common": "^v3.0.3", "tiny-hashes": "1.0.1" }, "engines": { @@ -18727,6 +18892,16 @@ } } }, + "node_modules/node-html-parser": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.6.tgz", + "integrity": "sha512-C/MGDQ2NjdjzUq41bW9kW00MPZecAe/oo89vZEFLDfWoQVDk/DdML1yuxVVKLDMFIFax2VTq6Vpfzyn7z5yYgQ==", + "dev": true, + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -18828,6 +19003,18 @@ "node": ">=4" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -20462,6 +20649,15 @@ "node": ">=4" } }, + "node_modules/regextras": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "dev": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/regjsgen": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", @@ -25310,11 +25506,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -25356,12 +25553,13 @@ } }, "@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -25442,9 +25640,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -25455,20 +25653,20 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -25555,22 +25753,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -25599,19 +25797,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -26303,39 +26501,39 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -26345,6 +26543,17 @@ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true }, + "@es-joy/jsdoccomment": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", + "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "dev": true, + "requires": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~2.2.5" + } + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -30264,6 +30473,12 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -31010,6 +31225,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -31299,9 +31520,9 @@ } }, "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "css": { "version": "3.0.0", @@ -31322,6 +31543,19 @@ } } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, "css-shorthand-properties": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", @@ -31334,6 +31568,12 @@ "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", "dev": true }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -31945,12 +32185,49 @@ "void-elements": "^2.0.0" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, "dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "dev": true }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dset": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", @@ -32181,6 +32458,12 @@ "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -32687,6 +32970,39 @@ } } }, + "eslint-plugin-jsdoc": { + "version": "38.1.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", + "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.22.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "regextras": "^0.8.0", + "semver": "^7.3.5", + "spdx-expression-parse": "^3.0.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -33695,9 +34011,9 @@ } }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true }, "for-each": { @@ -37283,11 +37599,6 @@ } } }, - "js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -37317,6 +37628,12 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "jsdoc-type-pratt-parser": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", + "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "dev": true + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -37909,26 +38226,16 @@ "dev": true }, "live-connect-common": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.0.tgz", - "integrity": "sha512-pa1SuzCg8ovsB6OziAQZpDid/OT8k37VgWFQkE8OUmG52Kf9PUtJM8wqaGdMXd/rNAe/NH8m+Kxx9MZuOvn5zg==" - }, - "live-connect-handlers": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/live-connect-handlers/-/live-connect-handlers-2.1.0.tgz", - "integrity": "sha512-uABe9D6yRp7HRgO6vhdIM5j88l17/ROzYGIOHc2Rv1TacLFH6IJ8sbmunY5mIJ9L6ArOVmL4WHY+QgOIkabhxg==", - "requires": { - "js-cookie": "^3.0.5", - "live-connect-common": "^3.0.0" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", + "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==" }, "live-connect-js": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.0.0.tgz", - "integrity": "sha512-4iMSeDPpueYoGEK4U152yKg+hj7jTxkzyfJUAGxtVHxdgjsjh8QmP3kLlTLBBrR/g/L/WCX47PBQxtGW9z8Rlw==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", + "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", "requires": { - "live-connect-common": "^3.0.0", - "live-connect-handlers": "^2.0.0", + "live-connect-common": "^v3.0.3", "tiny-hashes": "1.0.1" } }, @@ -39770,6 +40077,16 @@ "whatwg-url": "^5.0.0" } }, + "node-html-parser": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.6.tgz", + "integrity": "sha512-C/MGDQ2NjdjzUq41bW9kW00MPZecAe/oo89vZEFLDfWoQVDk/DdML1yuxVVKLDMFIFax2VTq6Vpfzyn7z5yYgQ==", + "dev": true, + "requires": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -39845,6 +40162,15 @@ } } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -41063,6 +41389,12 @@ "unicode-match-property-value-ecmascript": "^2.0.0" } }, + "regextras": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "dev": true + }, "regjsgen": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", diff --git a/package.json b/package.json index dfaf489b9c3..70b0bd4a144 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.12.0-pre", + "version": "8.33.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -58,6 +58,7 @@ "eslint": "^7.27.0", "eslint-config-standard": "^10.2.1", "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jsdoc": "^38.1.6", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prebid": "file:./plugins/eslint", "eslint-plugin-promise": "^5.1.0", @@ -101,6 +102,7 @@ "lodash": "^4.17.21", "mocha": "^10.0.0", "morgan": "^1.10.0", + "node-html-parser": "^6.1.5", "opn": "^5.4.0", "resolve-from": "^5.0.0", "sinon": "^4.1.3", @@ -126,13 +128,13 @@ "core-js": "^3.13.0", "core-js-pure": "^3.13.0", "criteo-direct-rsa-validate": "^1.1.0", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dlv": "1.1.3", "dset": "3.1.2", "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^6.0.0" + "live-connect-js": "^6.3.4" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/plugins/pbjsGlobals.js b/plugins/pbjsGlobals.js index 6d1eeb0c57d..62d29b567ed 100644 --- a/plugins/pbjsGlobals.js +++ b/plugins/pbjsGlobals.js @@ -34,7 +34,8 @@ module.exports = function(api, options) { '$$PREBID_GLOBAL$$': pbGlobal, '$$DEFINE_PREBID_GLOBAL$$': defineGlobal, '$$REPO_AND_VERSION$$': `${prebid.repository.url.split('/')[3]}_prebid_${prebid.version}`, - '$$PREBID_DIST_URL_BASE$$': options.prebidDistUrlBase || `https://cdn.jsdelivr.net/npm/prebid.js@${getNpmVersion(prebid.version)}/dist/` + '$$PREBID_DIST_URL_BASE$$': options.prebidDistUrlBase || `https://cdn.jsdelivr.net/npm/prebid.js@${getNpmVersion(prebid.version)}/dist/`, + '$$LIVE_INTENT_MODULE_MODE$$': (process && process.env && process.env.LiveConnectMode) || 'standard' }; let identifierToStringLiteral = [ diff --git a/src/activities/redactor.js b/src/activities/redactor.js index 5942ee17152..694a96b2b14 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -8,7 +8,17 @@ import { ACTIVITY_TRANSMIT_UFPD } from './activities.js'; -export const ORTB_UFPD_PATHS = ['user.data', 'user.ext.data', 'user.yob', 'user.gender', 'user.keywords', 'user.kwarray']; +export const ORTB_UFPD_PATHS = [ + 'data', + 'ext.data', + 'yob', + 'gender', + 'keywords', + 'kwarray', + 'id', + 'buyeruid', + 'customdata' +].map(f => `user.${f}`).concat('device.ext.cdep'); export const ORTB_EIDS_PATHS = ['user.eids', 'user.ext.eids']; export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; diff --git a/src/adRendering.js b/src/adRendering.js index 0a847d7cc25..f8fe0044f9b 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -1,33 +1,39 @@ -import {logError} from './utils.js'; +import {deepAccess, logError, logWarn, replaceMacros} from './utils.js'; import * as events from './events.js'; -import CONSTANTS from './constants.json'; +import constants from './constants.json'; +import {config} from './config.js'; +import {executeRenderer, isRendererRequired} from './Renderer.js'; +import {VIDEO} from './mediaTypes.js'; +import {auctionManager} from './auctionManager.js'; -const {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED} = CONSTANTS.EVENTS; +const {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON} = constants.EVENTS; /** * Emit the AD_RENDER_FAILED event. * - * @param reason one of the values in CONSTANTS.AD_RENDER_FAILED_REASON - * @param message failure description - * @param bid? bid response object that failed to render - * @param id? adId that failed to render + * @param {Object} data + * @param data.reason one of the values in CONSTANTS.AD_RENDER_FAILED_REASON + * @param data.message failure description + * @param [data.bid] bid response object that failed to render + * @param [data.id] adId that failed to render */ export function emitAdRenderFail({ reason, message, bid, id }) { const data = { reason, message }; if (bid) data.bid = bid; if (id) data.adId = id; - logError(message); + logError(`Error rendering ad (id: ${id}): ${message}`); events.emit(AD_RENDER_FAILED, data); } /** * Emit the AD_RENDER_SUCCEEDED event. * (Note: Invocation of this function indicates that the render function did not generate an error, it does not guarantee that tracking for this event has occurred yet.) - * @param doc document object that was used to `.write` the ad. Should be `null` if unavailable (e.g. for documents in + * @param {Object} data + * @param data.doc document object that was used to `.write` the ad. Should be `null` if unavailable (e.g. for documents in * a cross-origin frame). - * @param bid bid response object for the ad that was rendered - * @param id adId that was rendered. + * @param [data.bid] bid response object for the ad that was rendered + * @param [data.id] adId that was rendered. */ export function emitAdRenderSucceeded({ doc, bid, id }) { const data = { doc }; @@ -36,3 +42,61 @@ export function emitAdRenderSucceeded({ doc, bid, id }) { events.emit(AD_RENDER_SUCCEEDED, data); } + +export function handleRender(renderFn, {adId, options, bidResponse, doc}) { + if (bidResponse == null) { + emitAdRenderFail({ + reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + message: `Cannot find ad '${adId}'`, + id: adId + }); + return; + } + if (bidResponse.status === constants.BID_STATUS.RENDERED) { + logWarn(`Ad id ${adId} has been rendered before`); + events.emit(STALE_RENDER, bidResponse); + if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { + return; + } + } + try { + const {adId, ad, adUrl, width, height, renderer, cpm, originalCpm, mediaType} = bidResponse; + // rendering for outstream safeframe + if (isRendererRequired(renderer)) { + executeRenderer(renderer, bidResponse, doc); + } else if (adId) { + if (mediaType === VIDEO) { + emitAdRenderFail({ + reason: constants.AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, + message: 'Cannot render video ad', + bid: bidResponse, + id: adId + }); + return; + } + const repl = { + AUCTION_PRICE: originalCpm || cpm, + CLICKTHROUGH: options?.clickUrl || '' + }; + renderFn({ + ad: replaceMacros(ad, repl), + adUrl: replaceMacros(adUrl, repl), + adId, + width, + height + }); + } + } catch (e) { + emitAdRenderFail({ + reason: constants.AD_RENDER_FAILED_REASON.EXCEPTION, + message: e.message, + id: adId, + bid: bidResponse + }); + return; + } + // save winning bids + auctionManager.addWinningBid(bidResponse); + + events.emit(BID_WON, bidResponse); +} diff --git a/src/adServerManager.js b/src/adServerManager.js index af8fe34920e..7e1290b3983 100644 --- a/src/adServerManager.js +++ b/src/adServerManager.js @@ -34,7 +34,7 @@ const prebid = getGlobal(); /** * @typedef {Object} VideoSupport * - * @function {VideoAdUrlBuilder} buildVideoAdUrl + * @property {VideoAdUrlBuilder} buildVideoAdUrl */ /** diff --git a/src/adapterManager.js b/src/adapterManager.js index 45c4d890944..575d28b35fa 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -1,8 +1,6 @@ /** @module adaptermanger */ import { - _each, - bind, deepAccess, deepClone, flatten, @@ -31,12 +29,7 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import { - GDPR_GVLIDS, - gdprDataHandler, - uspDataHandler, - gppDataHandler, -} from './consentHandler.js'; +import {GDPR_GVLIDS, gdprDataHandler, gppDataHandler, uspDataHandler, } from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; @@ -380,13 +373,13 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request return; } - let [clientBidRequests, serverBidRequests] = bidRequests.reduce((partitions, bidRequest) => { + let [clientBidderRequests, serverBidderRequests] = bidRequests.reduce((partitions, bidRequest) => { partitions[Number(typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC)].push(bidRequest); return partitions; }, [[], []]); var uniqueServerBidRequests = []; - serverBidRequests.forEach(serverBidRequest => { + serverBidderRequests.forEach(serverBidRequest => { var index = -1; for (var i = 0; i < uniqueServerBidRequests.length; ++i) { if (serverBidRequest.uniquePbsTid === uniqueServerBidRequests[i].uniquePbsTid) { @@ -413,14 +406,17 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request let uniquePbsTid = uniqueServerBidRequests[counter].uniquePbsTid; let adUnitsS2SCopy = uniqueServerBidRequests[counter].adUnitsS2SCopy; - let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); + let uniqueServerRequests = serverBidderRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); if (s2sAdapter) { let s2sBidRequest = {'ad_units': adUnitsS2SCopy, s2sConfig, ortb2Fragments}; if (s2sBidRequest.ad_units.length) { let doneCbs = uniqueServerRequests.map(bidRequest => { bidRequest.start = timestamp(); - return doneCb.bind(bidRequest); + return function () { + onTimelyResponse(bidRequest.bidderRequestId); + doneCb.apply(bidRequest, arguments); + } }); const bidders = getBidderCodes(s2sBidRequest.ad_units).filter((bidder) => adaptersServerSide.includes(bidder)); @@ -435,7 +431,7 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request // make bid requests s2sAdapter.callBids( s2sBidRequest, - serverBidRequests, + serverBidderRequests, addBidResponse, () => doneCbs.forEach(done => done()), s2sAjax @@ -449,35 +445,34 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request }); // handle client adapter requests - clientBidRequests.forEach(bidRequest => { - bidRequest.start = timestamp(); + clientBidderRequests.forEach(bidderRequest => { + bidderRequest.start = timestamp(); // TODO : Do we check for bid in pool from here and skip calling adapter again ? - const adapter = _bidderRegistry[bidRequest.bidderCode]; - config.runWithBidder(bidRequest.bidderCode, () => { + const adapter = _bidderRegistry[bidderRequest.bidderCode]; + config.runWithBidder(bidderRequest.bidderCode, () => { logMessage(`CALLING BIDDER`); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidderRequest); }); let ajax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { - request: requestCallbacks.request.bind(null, bidRequest.bidderCode), + request: requestCallbacks.request.bind(null, bidderRequest.bidderCode), done: requestCallbacks.done } : undefined); - const adapterDone = doneCb.bind(bidRequest); + const adapterDone = doneCb.bind(bidderRequest); try { config.runWithBidder( - bidRequest.bidderCode, - bind.call( - adapter.callBids, + bidderRequest.bidderCode, + adapter.callBids.bind( adapter, - bidRequest, + bidderRequest, addBidResponse, adapterDone, ajax, - onTimelyResponse, - config.callbackWithBidder(bidRequest.bidderCode) + () => onTimelyResponse(bidderRequest.bidderRequestId), + config.callbackWithBidder(bidderRequest.bidderCode) ) ); } catch (e) { - logError(`${bidRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest}); + logError(`${bidderRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest: bidderRequest}); adapterDone(); } }); @@ -545,6 +540,9 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { } else { let spec = bidAdapter.getSpec(); let gvlid = options && options.gvlid; + if (spec.gvlid != null && gvlid == null) { + logWarn(`Alias '${alias}' will NOT re-use the GVL ID of the original adapter ('${spec.code}', gvlid: ${spec.gvlid}). Functionality that requires TCF consent may not work as expected.`) + } let skipPbsAliasing = options && options.skipPbsAliasing; newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid, skipPbsAliasing })); _aliasRegistry[alias] = bidderCode; @@ -591,7 +589,7 @@ adapterManager.enableAnalytics = function (config) { config = [config]; } - _each(config, adapterConfig => { + config.forEach(adapterConfig => { const entry = _analyticsRegistry[adapterConfig.provider]; if (entry && entry.adapter) { if (dep.isAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(MODULE_TYPE_ANALYTICS, adapterConfig.provider, {[ACTIVITY_PARAM_ANL_CONFIG]: adapterConfig}))) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index b31019a6d79..df7b2fb0eea 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -27,6 +27,11 @@ import {activityParams} from '../activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../activities/modules.js'; import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activities.js'; +/** + * @typedef {import('../mediaTypes.js').MediaType} MediaType + * @typedef {import('../Renderer.js').Renderer} Renderer + */ + /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. * @@ -57,7 +62,7 @@ import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activ * @property {string} code A code which will be used to uniquely identify this bidder. This should be the same * one as is used in the call to registerBidAdapter * @property {string[]} [aliases] A list of aliases which should also resolve to this bidder. - * @property {MediaType[]} [supportedMediaTypes]: A list of Media Types which the adapter supports. + * @property {MediaType[]} [supportedMediaTypes] A list of Media Types which the adapter supports. * @property {function(object): boolean} isBidRequestValid Determines whether or not the given bid has all the params * needed to make a valid request. * @property {function(BidRequest[], bidderRequest): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server @@ -105,7 +110,7 @@ import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activ * * @property {*} body The response body. If this is legal JSON, then it will be parsed. Otherwise it'll be a * string with the body's content. - * @property {{get: function(string): string} headers The response headers. + * @property {{get: function(string): string}} headers The response headers. * Call this like `ServerResponse.headers.get("Content-Type")` */ @@ -126,7 +131,7 @@ import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activ * @property {object} [video] Object for storing video response data * @property {object} [meta] Object for storing bid meta data * @property {string} [meta.primaryCatId] The IAB primary category ID - * @property [Renderer] renderer A Renderer which can be used as a default for this bid, + * @property {Renderer} renderer A Renderer which can be used as a default for this bid, * if the publisher doesn't override it. This is only relevant for Outstream Video bids. */ @@ -288,14 +293,11 @@ export function newBidder(spec) { onTimelyResponse(spec.code); responses.push(resp) }, - /** Process eventual BidderAuctionResponse.fledgeAuctionConfig field in response. - * @param {Array} fledgeAuctionConfigs - */ onFledgeAuctionConfigs: (fledgeAuctionConfigs) => { fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => { const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId]; if (bidRequest) { - addComponentAuction(bidRequest.adUnitCode, fledgeAuctionConfig.config); + addComponentAuction(bidRequest, fledgeAuctionConfig.config); } else { logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig); } @@ -460,7 +462,7 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe return Object.assign(defaults, ro, { browsingTopics: ro?.hasOwnProperty('browsingTopics') && !ro.browsingTopics ? false - : isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code)) + : (bidderSettings.get(spec.code, 'topicsHeader') ?? true) && isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code)) }) } switch (request.method) { @@ -528,7 +530,7 @@ export const registerSyncInner = hook('async', function(spec, responses, gdprCon } }, 'registerSyncs') -export const addComponentAuction = hook('sync', (adUnitCode, fledgeAuctionConfig) => { +export const addComponentAuction = hook('sync', (request, fledgeAuctionConfig) => { }, 'addComponentAuction'); // check that the bid has a width and height set diff --git a/src/adloader.js b/src/adloader.js index a87b930b7df..87683828aa8 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -27,6 +27,10 @@ const _approvedLoadExternalJSList = [ 'clean.io', 'a1Media', 'geoedge', + 'mediafilter', + 'qortex', + 'dynamicAdBoost', + 'contxtful' ] /** @@ -36,7 +40,7 @@ const _approvedLoadExternalJSList = [ * @param {string} moduleCode bidderCode or module code of the module requesting this resource * @param {function} [callback] callback function to be called after the script is loaded * @param {Document} [doc] the context document, in which the script will be loaded, defaults to loaded document - * @param {object} an object of attributes to be added to the script with setAttribute by [key] and [value]; Only the attributes passed in the first request of a url will be added. + * @param {object} attributes an object of attributes to be added to the script with setAttribute by [key] and [value]; Only the attributes passed in the first request of a url will be added. */ export function loadExternalScript(url, moduleCode, callback, doc, attributes) { if (!moduleCode || !url) { diff --git a/src/ajax.js b/src/ajax.js index 0601cc0e22b..6f2f45e935e 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -89,6 +89,17 @@ export function fetcherFactory(timeout = 3000, {request, done} = {}) { function toXHR({status, statusText = '', headers, url}, responseText) { let xml = 0; + function getXML(onError) { + if (xml === 0) { + try { + xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0]) + } catch (e) { + xml = null; + onError && onError(e) + } + } + return xml; + } return { readyState: XMLHttpRequest.DONE, status, @@ -98,17 +109,12 @@ function toXHR({status, statusText = '', headers, url}, responseText) { responseType: '', responseURL: url, get responseXML() { - if (xml === 0) { - try { - xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0]) - } catch (e) { - xml = null; - logError(e); - } - } - return xml; + return getXML(logError); }, getResponseHeader: (header) => headers?.has(header) ? headers.get(header) : null, + toJSON() { + return Object.assign({responseXML: getXML()}, this) + } } } diff --git a/src/auction.js b/src/auction.js index 48e1a8e3436..37bb2604167 100644 --- a/src/auction.js +++ b/src/auction.js @@ -9,14 +9,21 @@ */ /** - * @typedef {Object} AdUnit An object containing the adUnit configuration. - * - * @property {string} code A code which will be used to uniquely identify this bidder. This should be the same - * one as is used in the call to registerBidAdapter - * @property {Array.} sizes A list of size for adUnit. - * @property {object} params Any bidder-specific params which the publisher used in their bid request. - * This is guaranteed to have passed the spec.areParamsValid() test. - */ + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/config.js').MediaTypePriceGranularity} MediaTypePriceGranularity + * @typedef {import('../src/mediaTypes.js').MediaType} MediaType + */ + +/** + * @typedef {Object} AdUnit An object containing the adUnit configuration. + * + * @property {string} code A code which will be used to uniquely identify this bidder. This should be the same + * one as is used in the call to registerBidAdapter + * @property {Array.} sizes A list of size for adUnit. + * @property {object} params Any bidder-specific params which the publisher used in their bid request. + * This is guaranteed to have passed the spec.areParamsValid() test. + */ /** * @typedef {Array.} size @@ -58,11 +65,8 @@ */ import { - _each, - adUnitsFilter, - bind, + callBurl, deepAccess, - flatten, generateUUID, getValue, isEmpty, @@ -90,7 +94,7 @@ import {bidderSettings} from './bidderSettings.js'; import * as events from './events.js'; import adapterManager from './adapterManager.js'; import CONSTANTS from './constants.json'; -import {GreedyPromise} from './utils/promise.js'; +import {defer, GreedyPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; import {adjustCpm} from './utils/cpm.js'; import {getGlobal} from './prebidGlobal.js'; @@ -122,19 +126,20 @@ export function resetAuctionState() { } /** - * Creates new auction instance - * - * @param {Object} requestConfig - * @param {AdUnit} requestConfig.adUnits - * @param {AdUnitCode} requestConfig.adUnitCodes - * @param {function():void} requestConfig.callback - * @param {number} requestConfig.cbTimeout - * @param {Array.} requestConfig.labels - * @param {string} requestConfig.auctionId - * @param {{global: {}, bidder: {}}} ortb2Fragments first party data, separated into global - * (from getConfig('ortb2') + requestBids({ortb2})) and bidder (a map from bidderCode to ortb2) - * @returns {Auction} auction instance - */ + * Creates new auction instance + * + * @param {Object} requestConfig + * @param {AdUnit} requestConfig.adUnits + * @param {AdUnitCode} requestConfig.adUnitCodes + * @param {function():void} requestConfig.callback + * @param {number} requestConfig.cbTimeout + * @param {Array.} requestConfig.labels + * @param {string} requestConfig.auctionId + * @param {{global: {}, bidder: {}}} requestConfig.ortb2Fragments first party data, separated into global + * (from getConfig('ortb2') + requestBids({ortb2})) and bidder (a map from bidderCode to ortb2) + * @param {Object} requestConfig.metrics + * @returns {Auction} auction instance + */ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, auctionId, ortb2Fragments, metrics}) { metrics = useMetrics(metrics); const _adUnits = adUnits; @@ -142,7 +147,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a const _adUnitCodes = adUnitCodes; const _auctionId = auctionId || generateUUID(); const _timeout = cbTimeout; - const _timelyBidders = new Set(); + const _timelyRequests = new Set(); + const done = defer(); let _bidsRejected = []; let _callback = callback; let _bidderRequests = []; @@ -151,7 +157,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let _winningBids = []; let _auctionStart; let _auctionEnd; - let _timer; + let _timeoutTimer; let _auctionStatus; let _nonBids = []; @@ -182,25 +188,22 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function startAuctionTimer() { - const timedOut = true; - const timeoutCallback = executeCallback.bind(null, timedOut); - let timer = setTimeout(timeoutCallback, _timeout); - _timer = timer; + _timeoutTimer = setTimeout(() => executeCallback(true), _timeout); } - function executeCallback(timedOut, cleartimer) { - // clear timer when done calls executeCallback - if (cleartimer) { - clearTimeout(_timer); + function executeCallback(timedOut) { + if (!timedOut) { + clearTimeout(_timeoutTimer); + } else { + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, getProperties()); } - if (_auctionEnd === undefined) { - let timedOutBidders = []; + let timedOutRequests = []; if (timedOut) { logMessage(`Auction ${_auctionId} timedOut`); - timedOutBidders = getTimedOutBids(_bidderRequests, _timelyBidders); - if (timedOutBidders.length) { - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, timedOutBidders); + timedOutRequests = _bidderRequests.filter(rq => !_timelyRequests.has(rq.bidderRequestId)).flatMap(br => br.bids) + if (timedOutRequests.length) { + events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, timedOutRequests); } } @@ -209,14 +212,14 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a metrics.checkpoint('auctionEnd'); metrics.timeBetween('requestBids', 'auctionEnd', 'requestBids.total'); metrics.timeBetween('callBids', 'auctionEnd', 'requestBids.callBids'); + done.resolve(); events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties()); bidsBackCallback(_adUnits, function () { try { if (_callback != null) { - const adUnitCodes = _adUnitCodes; const bids = _bidsReceived - .filter(bind.call(adUnitsFilter, this, adUnitCodes)) + .filter(bid => _adUnitCodes.includes(bid.adUnitCode)) .reduce(groupByPlacement, {}); _callback.apply(pbjsInstance, [bids, timedOut, _auctionId]); _callback = null; @@ -225,8 +228,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a logError('Error executing bidsBackHandler', null, e); } finally { // Calling timed out bidders - if (timedOutBidders.length) { - adapterManager.callTimedOutBidders(adUnits, timedOutBidders, _timeout); + if (timedOutRequests.length) { + adapterManager.callTimedOutBidders(adUnits, timedOutRequests, _timeout); } // Only automatically sync if the publisher has not chosen to "enableOverride" let userSyncConfig = config.getConfig('userSync') || {}; @@ -244,11 +247,11 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a // when all bidders have called done callback atleast once it means auction is complete logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived); _auctionStatus = AUCTION_COMPLETED; - executeCallback(false, true); + executeCallback(false); } - function onTimelyResponse(bidderCode) { - _timelyBidders.add(bidderCode); + function onTimelyResponse(bidderRequestId) { + _timelyRequests.add(bidderRequestId); } function callBids() { @@ -368,6 +371,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a function addWinningBid(winningBid) { const winningAd = adUnits.find(adUnit => adUnit.transactionId === winningBid.transactionId); _winningBids = _winningBids.concat(winningBid); + callBurl(winningBid); adapterManager.callBidWonBidder(winningBid.adapterCode || winningBid.bidder, winningBid, adUnits); if (winningAd && !winningAd.deferBilling) adapterManager.callBidBillableBidder(winningBid); } @@ -386,12 +390,12 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a addBidReceived, addBidRejected, addNoBid, - executeCallback, callBids, addWinningBid, setBidTargeting, getWinningBids: () => _winningBids, getAuctionStart: () => _auctionStart, + getAuctionEnd: () => _auctionEnd, getTimeout: () => _timeout, getAuctionId: () => _auctionId, getAuctionStatus: () => _auctionStatus, @@ -403,20 +407,30 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a getNonBids: () => _nonBids, getFPD: () => ortb2Fragments, getMetrics: () => metrics, + end: done.promise }; } /** * Hook into this to intercept bids before they are added to an auction. * + * @type {Function} * @param adUnitCode * @param bid - * @param {function(String)} reject: a function that, when called, rejects `bid` with the given reason. + * @param {function(String): void} reject a function that, when called, rejects `bid` with the given reason. */ export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) { this.dispatch.call(null, adUnitCode, bid); }, 'addBidResponse'); +/** + * Delay hook for adapter responses. + * + * `ready` is a promise; auctions wait for it to resolve before closing. Modules can hook into this + * to delay the end of auctions while they perform initialization that does not need to delay their start. + */ +export const responsesReady = hook('sync', (ready) => ready, 'responsesReady'); + export const addBidderRequests = hook('sync', function(bidderRequests) { this.dispatch.call(this.context, bidderRequests); }, 'addBidderRequests'); @@ -432,32 +446,6 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM let allAdapterCalledDone = false; let bidderRequestsDone = new Set(); let bidResponseMap = {}; - const ready = {}; - - function waitFor(requestId, result) { - if (ready[requestId] == null) { - ready[requestId] = GreedyPromise.resolve(); - } - ready[requestId] = ready[requestId].then(() => GreedyPromise.resolve(result).catch(() => {})) - } - - function guard(bidderRequest, fn) { - let timeout = bidderRequest.timeout; - if (timeout == null || timeout > auctionInstance.getTimeout()) { - timeout = auctionInstance.getTimeout(); - } - const timeRemaining = auctionInstance.getAuctionStart() + timeout - Date.now(); - const wait = ready[bidderRequest.bidderRequestId]; - const orphanWait = ready['']; // also wait for "orphan" responses that are not associated with any request - if ((wait != null || orphanWait != null) && timeRemaining > 0) { - GreedyPromise.race([ - GreedyPromise.timeout(timeRemaining), - GreedyPromise.resolve(orphanWait).then(() => wait) - ]).then(fn); - } else { - fn(); - } - } function afterBidAdded() { outstandingBidsAdded--; @@ -476,7 +464,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM function acceptBidResponse(adUnitCode, bid) { handleBidResponse(adUnitCode, bid, (done) => { let bidResponse = getPreparedBidForAuction(bid); - + events.emit(CONSTANTS.EVENTS.BID_ACCEPTED, bidResponse); if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { tryAddVideoBid(auctionInstance, bidResponse, done); } else { @@ -532,8 +520,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM return { addBidResponse: (function () { function addBid(adUnitCode, bid) { - const bidderRequest = index.getBidderRequest(bid); - waitFor((bidderRequest && bidderRequest.bidderRequestId) || '', addBidResponse.call({ + addBidResponse.call({ dispatch: acceptBidResponse, }, adUnitCode, bid, (() => { let rejected = false; @@ -543,23 +530,17 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM rejected = true; } } - })())); + })()) } addBid.reject = rejectBidResponse; return addBid; })(), adapterDone: function () { - guard(this, adapterDone.bind(this)) + responsesReady(GreedyPromise.resolve()).finally(() => adapterDone.call(this)); } } } -export function doCallbacksIfTimedout(auctionInstance, bidResponse) { - if (bidResponse.timeToRespond > auctionInstance.getTimeout() + config.getConfig('timeoutBuffer')) { - auctionInstance.executeCallback(true); - } -} - // Add a bid to the auction. export function addBidToAuction(auctionInstance, bidResponse) { setupBidTargeting(bidResponse); @@ -567,8 +548,6 @@ export function addBidToAuction(auctionInstance, bidResponse) { useMetrics(bidResponse.metrics).timeSince('addBidResponse', 'addBidResponse.total'); auctionInstance.addBidReceived(bidResponse); events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } // Video bids may fail if the cache is down, or there's trouble on the network. @@ -615,16 +594,11 @@ const _storeInCache = (batch) => { const { auctionInstance, bidResponse, afterBidAdded } = batch[i]; if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } else { if (cacheId.uuid === '') { logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } else { bidResponse.videoCacheKey = cacheId.uuid; - if (!bidResponse.vastUrl) { bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); } @@ -785,8 +759,9 @@ export function getMediaTypeGranularity(mediaType, mediaTypes, mediaTypePriceGra /** * This function returns the price granularity defined. It can be either publisher defined or default value - * @param bid bid response object - * @param index + * @param {Bid} bid bid response object + * @param {object} obj + * @param {object} obj.index * @returns {string} granularity */ export const getPriceGranularity = (bid, {index = auctionManager.index} = {}) => { @@ -894,7 +869,6 @@ function defaultAdserverTargeting() { /** * @param {string} mediaType * @param {string} bidderCode - * @param {BidRequest} bidReq * @returns {*} */ export function getStandardBidderSettings(mediaType, bidderCode) { @@ -961,7 +935,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { var targeting = bidderSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]; custBidObj.size = custBidObj.getSize(); - _each(targeting, function (kvPair) { + (targeting || []).forEach(function (kvPair) { var key = kvPair.key; var value = kvPair.val; @@ -1014,24 +988,3 @@ function groupByPlacement(bidsByPlacement, bid) { bidsByPlacement[bid.adUnitCode].bids.push(bid); return bidsByPlacement; } - -/** - * Returns a list of bids that we haven't received a response yet where the bidder did not call done - * @param {BidRequest[]} bidderRequests List of bids requested for auction instance - * @param {Set} timelyBidders Set of bidders which responded in time - * - * @typedef {Object} TimedOutBid - * @property {string} bidId The id representing the bid - * @property {string} bidder The string name of the bidder - * @property {string} adUnitCode The code used to uniquely identify the ad unit on the publisher's page - * @property {string} auctionId The id representing the auction - * - * @return {Array} List of bids that Prebid hasn't received a response for - */ -function getTimedOutBids(bidderRequests, timelyBidders) { - const timedOutBids = bidderRequests - .map(bid => (bid.bids || []).filter(bid => !timelyBidders.has(bid.bidder))) - .reduce(flatten, []); - - return timedOutBids; -} diff --git a/src/auctionIndex.js b/src/auctionIndex.js index bdd2b42f9c6..bb3f535b41a 100644 --- a/src/auctionIndex.js +++ b/src/auctionIndex.js @@ -1,24 +1,30 @@ +/** + * @typedef {Object} AuctionIndex + * + * @property {function({ auctionId: * }): *} getAuction Returns auction instance for `auctionId` + * @property {function({ transactionId: * }): *} getAdUnit Returns `adUnit` object for `transactionId`. + * You should prefer `getMediaTypes` for looking up bid media types. + * @property {function({ transactionId: *, requestId: * }): *} getMediaTypes Returns mediaTypes object from bidRequest (through `requestId`) falling back to the adUnit (through `transactionId`). + * The bidRequest is given precedence because its mediaTypes can differ from the adUnit's (if bidder-specific labels are in use). + * Bids that have no associated request do not have labels either, and use the adUnit's mediaTypes. + * @property {function({ requestId: *, bidderRequestId: * }): *} getBidderRequest Returns bidderRequest that matches both requestId and bidderRequestId (if either or both are provided). + * Bid responses are not guaranteed to have a corresponding request. + * @property {function({ requestId: * }): *} getBidRequest Returns bidRequest object for requestId. + * Bid responses are not guaranteed to have a corresponding request. + */ + /** * Retrieves request-related bid data. * All methods are designed to work with Bid (response) objects returned by bid adapters. */ export function AuctionIndex(getAuctions) { Object.assign(this, { - /** - * @param auctionId - * @returns {*} Auction instance for `auctionId` - */ getAuction({auctionId}) { if (auctionId != null) { return getAuctions() .find(auction => auction.getAuctionId() === auctionId); } }, - /** - * NOTE: you should prefer {@link #getMediaTypes} for looking up bid media types. - * @param transactionId - * @returns adUnit object for `transactionId` - */ getAdUnit({transactionId}) { if (transactionId != null) { return getAuctions() @@ -26,14 +32,6 @@ export function AuctionIndex(getAuctions) { .find(au => au.transactionId === transactionId); } }, - /** - * @param transactionId - * @param requestId? - * @returns {*} mediaTypes object from bidRequest (through requestId) falling back to the adUnit (through transactionId). - * - * The bidRequest is given precedence because its mediaTypes can differ from the adUnit's (if bidder-specific labels are in use). - * Bids that have no associated request do not have labels either, and use the adUnit's mediaTypes. - */ getMediaTypes({transactionId, requestId}) { if (requestId != null) { const req = this.getBidRequest({requestId}); @@ -47,13 +45,6 @@ export function AuctionIndex(getAuctions) { } } }, - /** - * @param requestId? - * @param bidderRequestId? - * @returns {*} bidderRequest that matches both requestId and bidderRequestId (if either or both are provided). - * - * NOTE: Bid responses are not guaranteed to have a corresponding request. - */ getBidderRequest({requestId, bidderRequestId}) { if (requestId != null || bidderRequestId != null) { let bers = getAuctions().flatMap(a => a.getBidRequests()); @@ -67,12 +58,6 @@ export function AuctionIndex(getAuctions) { } } }, - /** - * @param requestId - * @returns {*} bidRequest object for requestId - * - * NOTE: Bid responses are not guaranteed to have a corresponding request. - */ getBidRequest({requestId}) { if (requestId != null) { return getAuctions() diff --git a/src/auctionManager.js b/src/auctionManager.js index 90d5fb543e2..8f3cbb56333 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -17,14 +17,19 @@ * @property {function(): Object} getStandardBidderAdServerTargeting - returns standard bidder targeting for all the adapters. Refer http://prebid.org/dev-docs/publisher-api-reference.html#module_pbjs.bidderSettings for more details * @property {function(Object): void} addWinningBid - add a winning bid to an auction based on auctionId * @property {function(): void} clearAllAuctions - clear all auctions for testing + * @property {AuctionIndex} index */ -import { uniques, flatten, logWarn } from './utils.js'; +import { uniques, logWarn } from './utils.js'; import { newAuction, getStandardBidderSettings, AUCTION_COMPLETED } from './auction.js'; -import {find} from './polyfill.js'; import {AuctionIndex} from './auctionIndex.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {getTTL, onTTLBufferChange} from './bidTTL.js'; +import {config} from './config.js'; + +const CACHE_TTL_SETTING = 'minBidCacheTTL'; /** * Creates new instance of auctionManager. There will only be one instance of auctionManager but @@ -33,15 +38,42 @@ import {useMetrics} from './utils/perfMetrics.js'; * @returns {AuctionManager} auctionManagerInstance */ export function newAuctionManager() { - const _auctions = []; + let minCacheTTL = null; + + const _auctions = ttlCollection({ + startTime: (au) => au.end.then(() => au.getAuctionEnd()), + ttl: (au) => minCacheTTL == null ? null : au.end.then(() => { + return Math.max(minCacheTTL, ...au.getBidsReceived().map(getTTL)) * 1000 + }), + }); + + onTTLBufferChange(() => { + if (minCacheTTL != null) _auctions.refresh(); + }) + + config.getConfig(CACHE_TTL_SETTING, (cfg) => { + const prev = minCacheTTL; + minCacheTTL = cfg?.[CACHE_TTL_SETTING]; + minCacheTTL = typeof minCacheTTL === 'number' ? minCacheTTL : null; + if (prev !== minCacheTTL) { + _auctions.refresh(); + } + }) + const auctionManager = {}; + function getAuction(auctionId) { + for (const auction of _auctions) { + if (auction.getAuctionId() === auctionId) return auction; + } + } + auctionManager.addWinningBid = function(bid) { const metrics = useMetrics(bid.metrics); metrics.checkpoint('bidWon'); metrics.timeBetween('auctionEnd', 'bidWon', 'render.pending'); metrics.timeBetween('requestBids', 'bidWon', 'render.e2e'); - const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId); + const auction = getAuction(bid.auctionId); if (auction) { bid.status = CONSTANTS.BID_STATUS.RENDERED; auction.addWinningBid(bid); @@ -50,48 +82,44 @@ export function newAuctionManager() { } }; - auctionManager.getAllWinningBids = function() { - return _auctions.map(auction => auction.getWinningBids()) - .reduce(flatten, []); - }; - - auctionManager.getBidsRequested = function() { - return _auctions.map(auction => auction.getBidRequests()) - .reduce(flatten, []); - }; - - auctionManager.getNoBids = function() { - return _auctions.map(auction => auction.getNoBids()) - .reduce(flatten, []); - }; - - auctionManager.getBidsReceived = function() { - return _auctions.map((auction) => { - if (auction.getAuctionStatus() === AUCTION_COMPLETED) { - return auction.getBidsReceived(); + Object.entries({ + getAllWinningBids: { + name: 'getWinningBids', + }, + getBidsRequested: { + name: 'getBidRequests' + }, + getNoBids: {}, + getAdUnits: {}, + getBidsReceived: { + pre(auction) { + return auction.getAuctionStatus() === AUCTION_COMPLETED; } - }).reduce(flatten, []) - .filter(bid => bid); - }; + }, + getAdUnitCodes: { + post: uniques, + } + }).forEach(([mgrMethod, {name = mgrMethod, pre, post}]) => { + const mapper = pre == null + ? (auction) => auction[name]() + : (auction) => pre(auction) ? auction[name]() : []; + const filter = post == null + ? (items) => items + : (items) => items.filter(post) + auctionManager[mgrMethod] = () => { + return filter(_auctions.toArray().flatMap(mapper)); + } + }) + + function allBidsReceived() { + return _auctions.toArray().flatMap(au => au.getBidsReceived()) + } auctionManager.getAllBidsForAdUnitCode = function(adUnitCode) { - return _auctions.map((auction) => { - return auction.getBidsReceived(); - }).reduce(flatten, []) + return allBidsReceived() .filter(bid => bid && bid.adUnitCode === adUnitCode) }; - auctionManager.getAdUnits = function() { - return _auctions.map(auction => auction.getAdUnits()) - .reduce(flatten, []); - }; - - auctionManager.getAdUnitCodes = function() { - return _auctions.map(auction => auction.getAdUnitCodes()) - .reduce(flatten, []) - .filter(uniques); - }; - auctionManager.createAuction = function(opts) { const auction = newAuction(opts); _addAuction(auction); @@ -99,7 +127,8 @@ export function newAuctionManager() { }; auctionManager.findBidByAdId = function(adId) { - return find(_auctions.map(auction => auction.getBidsReceived()).reduce(flatten, []), bid => bid.adId === adId); + return allBidsReceived() + .find(bid => bid.adId === adId); }; auctionManager.getStandardBidderAdServerTargeting = function() { @@ -111,24 +140,25 @@ export function newAuctionManager() { if (bid) bid.status = status; if (bid && status === CONSTANTS.BID_STATUS.BID_TARGETING_SET) { - const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId); + const auction = getAuction(bid.auctionId); if (auction) auction.setBidTargeting(bid); } } auctionManager.getLastAuctionId = function() { - return _auctions.length && _auctions[_auctions.length - 1].getAuctionId() + const auctions = _auctions.toArray(); + return auctions.length && auctions[auctions.length - 1].getAuctionId() }; auctionManager.clearAllAuctions = function() { - _auctions.length = 0; + _auctions.clear(); } function _addAuction(auction) { - _auctions.push(auction); + _auctions.add(auction); } - auctionManager.index = new AuctionIndex(() => _auctions); + auctionManager.index = new AuctionIndex(() => _auctions.toArray()); return auctionManager; } diff --git a/src/bidTTL.js b/src/bidTTL.js new file mode 100644 index 00000000000..55ba0c026b0 --- /dev/null +++ b/src/bidTTL.js @@ -0,0 +1,25 @@ +import {config} from './config.js'; +import {logError} from './utils.js'; +let TTL_BUFFER = 1; + +const listeners = []; + +config.getConfig('ttlBuffer', (cfg) => { + if (typeof cfg.ttlBuffer === 'number') { + const prev = TTL_BUFFER; + TTL_BUFFER = cfg.ttlBuffer; + if (prev !== TTL_BUFFER) { + listeners.forEach(l => l(TTL_BUFFER)) + } + } else { + logError('Invalid value for ttlBuffer', cfg.ttlBuffer); + } +}) + +export function getTTL(bid) { + return bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : TTL_BUFFER); +} + +export function onTTLBufferChange(listener) { + listeners.push(listener); +} diff --git a/src/config.js b/src/config.js index 48909d677e9..e3bb5f146ed 100644 --- a/src/config.js +++ b/src/config.js @@ -12,11 +12,20 @@ * @property {(string|Object)} [video-outstream] */ -import { isValidPriceConfig } from './cpmBucketManager.js'; -import {find, includes, arrayFrom as from} from './polyfill.js'; +import {isValidPriceConfig} from './cpmBucketManager.js'; +import {arrayFrom as from, find, includes} from './polyfill.js'; import { - mergeDeep, deepClone, getParameterByName, isPlainObject, logMessage, logWarn, logError, - isArray, isStr, isBoolean, deepAccess, bind + deepAccess, + deepClone, + getParameterByName, + isArray, + isBoolean, + isPlainObject, + isStr, + logError, + logMessage, + logWarn, + mergeDeep } from './utils.js'; import CONSTANTS from './constants.json'; @@ -50,13 +59,6 @@ const GRANULARITY_OPTIONS = { const ALL_TOPICS = '*'; -/** - * @typedef {object} PrebidConfig - * - * @property {string} cache.url Set a url if we should use prebid-cache to store video bids before adding - * bids to the auction. **NOTE** This must be set if you want to use the dfpAdServerVideo module. - */ - export function newConfig() { let listeners = []; let defaults; @@ -505,7 +507,7 @@ export function newConfig() { return function(cb) { return function(...args) { if (typeof cb === 'function') { - return runWithBidder(bidder, bind.call(cb, this, ...args)) + return runWithBidder(bidder, cb.bind(this, ...args)) } else { logWarn('config.callbackWithBidder callback is not a function'); } @@ -542,4 +544,8 @@ export function newConfig() { }; } +/** + * Set a `cache.url` if we should use prebid-cache to store video bids before adding bids to the auction. + * This must be set if you want to use the dfpAdServerVideo module. + */ export const config = newConfig(); diff --git a/src/consentHandler.js b/src/consentHandler.js index 9e3ee5b4c40..5b5d8b805cd 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -10,6 +10,14 @@ import {config} from './config.js'; */ export const VENDORLESS_GVLID = Object.freeze({}); +/** + * Placeholder gvlid for when device.ext.cdep is present (Privacy Sandbox cookie deprecation label). When this value is used as gvlid, the gdpr + * enforcement module will look to see that publisher consent was given. + * + * see https://github.com/prebid/Prebid.js/issues/10516 + */ +export const FIRST_PARTY_GVLID = Object.freeze({}); + export class ConsentHandler { #enabled; #data; diff --git a/src/constants.json b/src/constants.json index c763090f6d0..24882c160f8 100644 --- a/src/constants.json +++ b/src/constants.json @@ -23,6 +23,7 @@ }, "EVENTS": { "AUCTION_INIT": "auctionInit", + "AUCTION_TIMEOUT": "auctionTimeout", "AUCTION_END": "auctionEnd", "BID_ADJUSTMENT": "bidAdjustment", "BID_TIMEOUT": "bidTimeout", @@ -45,7 +46,8 @@ "AUCTION_DEBUG": "auctionDebug", "BID_VIEWABLE": "bidViewable", "STALE_RENDER": "staleRender", - "BILLABLE_EVENT": "billableEvent" + "BILLABLE_EVENT": "billableEvent", + "BID_ACCEPTED": "bidAccepted" }, "AD_RENDER_FAILED_REASON": { "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocument", @@ -175,6 +177,8 @@ "AD_UNIT": "adUnit", "SET_CONFIG": "setConfig", "FETCH": "fetch", - "SUCCESS": "success" + "SUCCESS": "success", + "ERROR": "error", + "TIMEOUT": "timeout" } } diff --git a/src/events.js b/src/events.js index 62f8c070deb..d98991180bf 100644 --- a/src/events.js +++ b/src/events.js @@ -3,23 +3,38 @@ */ import * as utils from './utils.js' import CONSTANTS from './constants.json'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {config} from './config.js'; +const TTL_CONFIG = 'eventHistoryTTL'; -var slice = Array.prototype.slice; -var push = Array.prototype.push; +let eventTTL = null; -// define entire events -// var allEvents = ['bidRequested','bidResponse','bidWon','bidTimeout']; -var allEvents = utils._map(CONSTANTS.EVENTS, function (v) { - return v; +// keep a record of all events fired +const eventsFired = ttlCollection({ + monotonic: true, + ttl: () => eventTTL, +}) + +config.getConfig(TTL_CONFIG, (val) => { + const previous = eventTTL; + val = val?.[TTL_CONFIG]; + eventTTL = typeof val === 'number' ? val * 1000 : null; + if (previous !== eventTTL) { + eventsFired.refresh(); + } }); -var idPaths = CONSTANTS.EVENT_ID_PATHS; +let slice = Array.prototype.slice; +let push = Array.prototype.push; + +// define entire events +let allEvents = Object.values(CONSTANTS.EVENTS); + +const idPaths = CONSTANTS.EVENT_ID_PATHS; -// keep a record of all events fired -var eventsFired = []; const _public = (function () { - var _handlers = {}; - var _public = {}; + let _handlers = {}; + let _public = {}; /** * @@ -30,31 +45,30 @@ const _public = (function () { function _dispatch(eventString, args) { utils.logMessage('Emitting event for: ' + eventString); - var eventPayload = args[0] || {}; - var idPath = idPaths[eventString]; - var key = eventPayload[idPath]; - var event = _handlers[eventString] || { que: [] }; - var eventKeys = utils._map(event, function (v, k) { - return k; - }); + let eventPayload = args[0] || {}; + let idPath = idPaths[eventString]; + let key = eventPayload[idPath]; + let event = _handlers[eventString] || { que: [] }; + var eventKeys = Object.keys(event); - var callbacks = []; + let callbacks = []; // record the event: - eventsFired.push({ + eventsFired.add({ eventType: eventString, args: eventPayload, id: key, elapsedTime: utils.getPerformanceNow(), }); - /** Push each specific callback to the `callbacks` array. + /** + * Push each specific callback to the `callbacks` array. * If the `event` map has a key that matches the value of the * event payload id path, e.g. `eventPayload[idPath]`, then apply * each function in the `que` array as an argument to push to the * `callbacks` array - * */ - if (key && utils.contains(eventKeys, key)) { + */ + if (key && eventKeys.includes(key)) { push.apply(callbacks, event[key].que); } @@ -62,24 +76,26 @@ const _public = (function () { push.apply(callbacks, event.que); /** call each of the callbacks */ - utils._each(callbacks, function (fn) { + (callbacks || []).forEach(function (fn) { if (!fn) return; try { fn.apply(null, args); } catch (e) { - utils.logError('Error executing handler:', 'events.js', e); + utils.logError('Error executing handler:', 'events.js', e, eventString); } }); } function _checkAvailableEvent(event) { - return utils.contains(allEvents, event); + return allEvents.includes(event) } + _public.has = _checkAvailableEvent; + _public.on = function (eventString, handler, id) { // check whether available event or not if (_checkAvailableEvent(eventString)) { - var event = _handlers[eventString] || { que: [] }; + let event = _handlers[eventString] || { que: [] }; if (id) { event[id] = event[id] || { que: [] }; @@ -95,12 +111,12 @@ const _public = (function () { }; _public.emit = function (event) { - var args = slice.call(arguments, 1); + let args = slice.call(arguments, 1); _dispatch(event, args); }; _public.off = function (eventString, handler, id) { - var event = _handlers[eventString]; + let event = _handlers[eventString]; if (utils.isEmpty(event) || (utils.isEmpty(event.que) && utils.isEmpty(event[id]))) { return; @@ -111,15 +127,15 @@ const _public = (function () { } if (id) { - utils._each(event[id].que, function (_handler) { - var que = event[id].que; + (event[id].que || []).forEach(function (_handler) { + let que = event[id].que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } }); } else { - utils._each(event.que, function (_handler) { - var que = event.que; + (event.que || []).forEach(function (_handler) { + let que = event.que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } @@ -142,13 +158,7 @@ const _public = (function () { * @return {Array} array of events fired */ _public.getEvents = function () { - var arrayCopy = []; - utils._each(eventsFired, function (value) { - var newProp = Object.assign({}, value); - arrayCopy.push(newProp); - }); - - return arrayCopy; + return eventsFired.toArray().map(val => Object.assign({}, val)) }; return _public; @@ -156,8 +166,8 @@ const _public = (function () { utils._setEventEmitter(_public.emit.bind(_public)); -export const {on, off, get, getEvents, emit, addEvents} = _public; +export const {on, off, get, getEvents, emit, addEvents, has} = _public; export function clearEvents() { - eventsFired.length = 0; + eventsFired.clear(); } diff --git a/src/fpd/enrichment.js b/src/fpd/enrichment.js index f812d8435d9..911509455e0 100644 --- a/src/fpd/enrichment.js +++ b/src/fpd/enrichment.js @@ -6,6 +6,10 @@ import {config} from '../config.js'; import {getHighEntropySUA, getLowEntropySUA} from './sua.js'; import {GreedyPromise} from '../utils/promise.js'; import {CLIENT_SECTIONS, clientSectionChecker, hasSection} from './oneClient.js'; +import {isActivityAllowed} from '../activities/rules.js'; +import {activityParams} from '../activities/activityParams.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../activities/activities.js'; +import {MODULE_TYPE_PREBID} from '../activities/modules.js'; export const dep = { getRefererInfo, @@ -24,8 +28,10 @@ const oneClient = clientSectionChecker('FPD') * @returns: {Promise[{}]}: a promise to an enriched ortb2 object. */ export const enrichFPD = hook('sync', (fpd) => { - return GreedyPromise.all([fpd, getSUA().catch(() => null)]) - .then(([ortb2, sua]) => { + const promArr = [fpd, getSUA().catch(() => null), tryToGetCdepLabel().catch(() => null)]; + + return GreedyPromise.all(promArr) + .then(([ortb2, sua, cdep]) => { const ri = dep.getRefererInfo(); mergeLegacySetConfigs(ortb2); Object.entries(ENRICHMENTS).forEach(([section, getEnrichments]) => { @@ -34,9 +40,18 @@ export const enrichFPD = hook('sync', (fpd) => { ortb2[section] = mergeDeep({}, data, ortb2[section]); } }); + if (sua) { deepSetValue(ortb2, 'device.sua', Object.assign({}, sua, ortb2.device.sua)); } + + if (cdep) { + const ext = { + cdep + } + deepSetValue(ortb2, 'device.ext', Object.assign({}, ext, ortb2.device.ext)); + } + ortb2 = oneClient(ortb2); for (let section of CLIENT_SECTIONS) { if (hasSection(ortb2, section)) { @@ -44,6 +59,7 @@ export const enrichFPD = hook('sync', (fpd) => { break; } } + return ortb2; }); }); @@ -78,6 +94,10 @@ function removeUndef(obj) { return getDefinedParams(obj, Object.keys(obj)) } +function tryToGetCdepLabel() { + return GreedyPromise.resolve('cookieDeprecationLabel' in navigator && isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && navigator.cookieDeprecationLabel.getValue()); +} + const ENRICHMENTS = { site(ortb2, ri) { if (CLIENT_SECTIONS.filter(p => p !== 'site').some(hasSection.bind(null, ortb2))) { @@ -93,6 +113,7 @@ const ENRICHMENTS = { return winFallback((win) => { const w = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; const h = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; + return { w, h, diff --git a/src/mediaTypes.js b/src/mediaTypes.js index eea286f7af5..2afa2aefaf9 100644 --- a/src/mediaTypes.js +++ b/src/mediaTypes.js @@ -10,11 +10,11 @@ * @typedef {('adpod')} VideoContext */ -/** @type MediaType */ +/** @type {MediaType} */ export const NATIVE = 'native'; -/** @type MediaType */ +/** @type {MediaType} */ export const VIDEO = 'video'; -/** @type MediaType */ +/** @type {MediaType} */ export const BANNER = 'banner'; -/** @type VideoContext */ +/** @type {VideoContext} */ export const ADPOD = 'adpod'; diff --git a/src/native.js b/src/native.js index 1414c7a2ee7..c4709dd77e2 100644 --- a/src/native.js +++ b/src/native.js @@ -1,7 +1,6 @@ import { deepAccess, deepClone, - getKeyByValue, insertHtmlIntoIframe, isArray, isBoolean, @@ -422,12 +421,14 @@ function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {} return message; } +const NATIVE_KEYS_INVERTED = Object.fromEntries(Object.entries(CONSTANTS.NATIVE_KEYS).map(([k, v]) => [v, k])); + /** * Constructs a message object containing asset values for each of the * requested data keys. */ export function getAssetMessage(data, adObject) { - const keys = data.assets.map((k) => getKeyByValue(CONSTANTS.NATIVE_KEYS, k)); + const keys = data.assets.map((k) => NATIVE_KEYS_INVERTED[k]); return assetsMessage(data, adObject, keys); } diff --git a/src/prebid.js b/src/prebid.js index 94ade8e5f83..df224ab83cf 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -2,19 +2,11 @@ import {getGlobal} from './prebidGlobal.js'; import { - adUnitsFilter, - bind, - callBurl, - contains, - createInvisibleIframe, deepAccess, deepClone, deepSetValue, flatten, generateUUID, - getHighestCpm, - inIframe, - insertElement, isArray, isArrayOfNums, isEmpty, @@ -26,8 +18,6 @@ import { logMessage, logWarn, mergeDeep, - replaceAuctionPrice, - replaceClickThrough, transformAdServerTargetingObj, uniques, unsupportedBidderMessage @@ -41,10 +31,8 @@ import {hook, wrapHook} from './hook.js'; import {loadSession} from './debugging.js'; import {includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; -import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; -import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; import {default as adapterManager, getS2SBidderSet} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; @@ -52,13 +40,15 @@ 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 {getHighestCpm} from './utils/reducers.js'; +import {fillVideoDefaults} from './video.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; /* private variables */ -const { ADD_AD_UNITS, BID_WON, REQUEST_BIDS, SET_TARGETING, STALE_RENDER } = CONSTANTS.EVENTS; -const { PREVENT_WRITING_ON_MAIN_DOCUMENT, NO_AD, EXCEPTION, CANNOT_FIND_AD, MISSING_DOC_OR_ADID } = CONSTANTS.AD_RENDER_FAILED_REASON; +const { ADD_AD_UNITS, REQUEST_BIDS, SET_TARGETING } = CONSTANTS.EVENTS; const eventValidators = { bidWon: checkDefinedPlacement @@ -90,7 +80,7 @@ function checkDefinedPlacement(id) { .reduce(flatten) .filter(uniques); - if (!contains(adUnitCodes, id)) { + if (!adUnitCodes.includes(id)) { logError('The "' + id + '" placement is not defined.'); return; } @@ -98,13 +88,6 @@ function checkDefinedPlacement(id) { return true; } -function setRenderSize(doc, width, height) { - if (doc.defaultView && doc.defaultView.frameElement) { - doc.defaultView.frameElement.width = width; - doc.defaultView.frameElement.height = height; - } -} - function validateSizes(sizes, targLength) { let cleanSizes = []; if (isArray(sizes) && ((targLength) ? sizes.length === targLength : sizes.length > 0)) { @@ -269,6 +252,12 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { return validatedAdUnits; }, 'checkAdUnitSetup'); +function fillAdUnitDefaults(adUnits) { + if (FEATURES.VIDEO) { + adUnits.forEach(au => fillVideoDefaults(au)) + } +} + /// /////////////////////////////// // // // Start Public APIs // @@ -338,7 +327,7 @@ pbjsInstance.getConsentMetadata = function () { function getBids(type) { const responses = auctionManager[type]() - .filter(bind.call(adUnitsFilter, this, auctionManager.getAdUnitCodes())); + .filter(bid => auctionManager.getAdUnitCodes().includes(bid.adUnitCode)) // find the last auction id to get responses for most recent auction only const currentAuctionId = auctionManager.getLastAuctionId(); @@ -454,19 +443,6 @@ pbjsInstance.setTargetingForAst = function (adUnitCodes) { events.emit(SET_TARGETING, targeting.getAllTargeting()); }; -/** - * This function will check for presence of given node in given parent. If not present - will inject it. - * @param {Node} node node, whose existance is in question - * @param {Document} doc document element do look in - * @param {string} tagName tag name to look in - */ -function reinjectNodeIfRemoved(node, doc, tagName) { - const injectionNode = doc.querySelector(tagName); - if (!node.parentNode || node.parentNode !== injectionNode) { - insertElement(node, doc, tagName); - } -} - /** * This function will render the ad (based on params) in the given iframe document passed through. * Note that doc SHOULD NOT be the parent document page as we can't doc.write() asynchronously @@ -477,103 +453,7 @@ function reinjectNodeIfRemoved(node, doc, tagName) { pbjsInstance.renderAd = hook('async', function (doc, id, options) { logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); logMessage('Calling renderAd with adId :' + id); - - if (!id) { - const message = `Error trying to write ad Id :${id} to the page. Missing adId`; - emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id }); - return; - } - - try { - // lookup ad by ad Id - const bid = auctionManager.findBidByAdId(id); - if (!bid) { - const message = `Error trying to write ad. Cannot find ad by given id : ${id}`; - emitAdRenderFail({ reason: CANNOT_FIND_AD, message, id }); - return; - } - - if (bid.status === CONSTANTS.BID_STATUS.RENDERED) { - logWarn(`Ad id ${bid.adId} has been rendered before`); - events.emit(STALE_RENDER, bid); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } - } - - // replace macros according to openRTB with price paid = bid.cpm - bid.ad = replaceAuctionPrice(bid.ad, bid.originalCpm || bid.cpm); - bid.adUrl = replaceAuctionPrice(bid.adUrl, bid.originalCpm || bid.cpm); - // replacing clickthrough if submitted - if (options && options.clickThrough) { - const {clickThrough} = options; - bid.ad = replaceClickThrough(bid.ad, clickThrough); - bid.adUrl = replaceClickThrough(bid.adUrl, clickThrough); - } - - // save winning bids - auctionManager.addWinningBid(bid); - - // emit 'bid won' event here - events.emit(BID_WON, bid); - - const {height, width, ad, mediaType, adUrl, renderer} = bid; - - // video module - if (FEATURES.VIDEO) { - const adUnitCode = bid.adUnitCode; - const adUnit = pbjsInstance.adUnits.filter(adUnit => adUnit.code === adUnitCode); - const videoModule = pbjsInstance.videoModule; - if (adUnit.video && videoModule) { - videoModule.renderBid(adUnit.video.divId, bid); - return; - } - } - - if (!doc) { - const message = `Error trying to write ad Id :${id} to the page. Missing document`; - emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id }); - return; - } - - const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`); - insertElement(creativeComment, doc, 'html'); - - if (isRendererRequired(renderer)) { - executeRenderer(renderer, bid, doc); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - emitAdRenderSucceeded({ doc, bid, id }); - } else if ((doc === document && !inIframe()) || mediaType === 'video') { - const message = `Error trying to write ad. Ad render call ad id ${id} was prevented from writing to the main document.`; - emitAdRenderFail({reason: PREVENT_WRITING_ON_MAIN_DOCUMENT, message, bid, id}); - } else if (ad) { - doc.write(ad); - doc.close(); - setRenderSize(doc, width, height); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - callBurl(bid); - emitAdRenderSucceeded({ doc, bid, id }); - } else if (adUrl) { - const iframe = createInvisibleIframe(); - iframe.height = height; - iframe.width = width; - iframe.style.display = 'inline'; - iframe.style.overflow = 'hidden'; - iframe.src = adUrl; - - insertElement(iframe, doc, 'body'); - setRenderSize(doc, width, height); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - callBurl(bid); - emitAdRenderSucceeded({ doc, bid, id }); - } else { - const message = `Error trying to write ad. No ad for bid response id: ${id}`; - emitAdRenderFail({reason: NO_AD, message, bid, id}); - } - } catch (e) { - const message = `Error trying to write ad Id :${id} to the page:${e.message}`; - emitAdRenderFail({ reason: EXCEPTION, message, id }); - } + renderAdDirect(doc, id, options); }); /** @@ -658,6 +538,7 @@ pbjsInstance.requestBids = (function() { export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, ttlBuffer, adUnitCodes, labels, auctionId, ortb2Fragments, metrics, defer } = {}) { const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); + fillAdUnitDefaults(adUnits); adUnits = useMetrics(metrics).measureTime('requestBids.validate', () => checkAdUnitSetup(adUnits)); function auctionDone(bids, timedOut, auctionId) { diff --git a/src/secureCreatives.js b/src/secureCreatives.js index c719bc191f2..0ea93e7e4fb 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -6,26 +6,24 @@ import * as events from './events.js'; import {fireNativeTrackers, getAllAssetsMessage, getAssetMessage} from './native.js'; import constants from './constants.json'; -import {deepAccess, isApnGetTagDefined, isGptPubadsDefined, logError, logWarn, replaceAuctionPrice} from './utils.js'; +import {isApnGetTagDefined, isGptPubadsDefined, logError, logWarn} from './utils.js'; import {auctionManager} from './auctionManager.js'; import {find, includes} from './polyfill.js'; -import {executeRenderer, isRendererRequired} from './Renderer.js'; -import {config} from './config.js'; -import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; +import {emitAdRenderFail, emitAdRenderSucceeded, handleRender} from './adRendering.js'; +import {PREBID_EVENT, PREBID_NATIVE, PREBID_REQUEST, PREBID_RESPONSE} from '../libraries/creativeRender/constants.js'; const BID_WON = constants.EVENTS.BID_WON; -const STALE_RENDER = constants.EVENTS.STALE_RENDER; const WON_AD_IDS = new WeakSet(); const HANDLER_MAP = { - 'Prebid Request': handleRenderRequest, - 'Prebid Event': handleEventRequest, -} + [PREBID_REQUEST]: handleRenderRequest, + [PREBID_EVENT]: handleEventRequest, +}; if (FEATURES.NATIVE) { Object.assign(HANDLER_MAP, { - 'Prebid Native': handleNativeRequest, - }) + [PREBID_NATIVE]: handleNativeRequest, + }); } export function listenMessagesFromCreative() { @@ -35,18 +33,18 @@ export function listenMessagesFromCreative() { export function getReplier(ev) { if (ev.origin == null && ev.ports.length === 0) { return function () { - const msg = 'Cannot post message to a frame with null origin. Please update creatives to use MessageChannel, see https://github.com/prebid/Prebid.js/issues/7870' - logError(msg) + const msg = 'Cannot post message to a frame with null origin. Please update creatives to use MessageChannel, see https://github.com/prebid/Prebid.js/issues/7870'; + logError(msg); throw new Error(msg); - } + }; } else if (ev.ports.length > 0) { return function (message) { ev.ports[0].postMessage(JSON.stringify(message)); - } + }; } else { return function (message) { ev.source.postMessage(JSON.stringify(message), ev.origin); - } + }; } } @@ -69,39 +67,13 @@ export function receiveMessage(ev) { } } -function handleRenderRequest(reply, data, adObject) { - if (adObject == null) { - emitAdRenderFail({ - reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - message: `Cannot find ad for cross-origin render request: '${data.adId}'`, - id: data.adId - }); - return; - } - if (adObject.status === constants.BID_STATUS.RENDERED) { - logWarn(`Ad id ${adObject.adId} has been rendered before`); - events.emit(STALE_RENDER, adObject); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } - } - - try { - _sendAdToCreative(adObject, reply); - } catch (e) { - emitAdRenderFail({ - reason: constants.AD_RENDER_FAILED_REASON.EXCEPTION, - message: e.message, - id: data.adId, - bid: adObject - }); - return; - } - - // save winning bids - auctionManager.addWinningBid(adObject); - - events.emit(BID_WON, adObject); +function handleRenderRequest(reply, message, bidResponse) { + handleRender(function (adData) { + resizeRemoteCreative(bidResponse); + reply(Object.assign({ + message: PREBID_RESPONSE, + }, adData)); + }, {options: message.options, adId: message.adId, bidResponse}); } function handleNativeRequest(reply, data, adObject) { @@ -164,29 +136,11 @@ function handleEventRequest(reply, data, adObject) { }); break; default: - logError(`Received x-origin event request for unsupported event: '${data.event}' (adId: '${data.adId}')`) - } -} - -export function _sendAdToCreative(adObject, reply) { - const { adId, ad, adUrl, width, height, renderer, cpm, originalCpm } = adObject; - // rendering for outstream safeframe - if (isRendererRequired(renderer)) { - executeRenderer(renderer, adObject); - } else if (adId) { - resizeRemoteCreative(adObject); - reply({ - message: 'Prebid Response', - ad: replaceAuctionPrice(ad, originalCpm || cpm), - adUrl: replaceAuctionPrice(adUrl, originalCpm || cpm), - adId, - width, - height - }); + logError(`Received x-origin event request for unsupported event: '${data.event}' (adId: '${data.adId}')`); } } -function resizeRemoteCreative({ adId, adUnitCode, width, height }) { +export function resizeRemoteCreative({adId, adUnitCode, width, height}) { // resize both container div + iframe ['div', 'iframe'].forEach(elmType => { // not select element that gets removed after dfp render @@ -208,9 +162,9 @@ function resizeRemoteCreative({ adId, adUnitCode, width, height }) { function getElementIdBasedOnAdServer(adId, adUnitCode) { if (isGptPubadsDefined()) { - return getDfpElementId(adId) + return getDfpElementId(adId); } else if (isApnGetTagDefined()) { - return getAstElementId(adUnitCode) + return getAstElementId(adUnitCode); } else { return adUnitCode; } diff --git a/src/targeting.js b/src/targeting.js index a75c9a2b52f..2c2ddefa6c2 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,8 +1,6 @@ import { deepAccess, deepClone, - getHighestCpm, - getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, isArray, @@ -24,19 +22,12 @@ import {hook} from './hook.js'; import {bidderSettings} from './bidderSettings.js'; import {find, includes} from './polyfill.js'; import CONSTANTS from './constants.json'; +import {getHighestCpm, getOldestHighestCpmBid} from './utils/reducers.js'; +import {getTTL} from './bidTTL.js'; var pbTargetingKeys = []; const MAX_DFP_KEYLENGTH = 20; -let DEFAULT_TTL_BUFFER = 1; - -config.getConfig('ttlBuffer', (cfg) => { - if (typeof cfg.ttlBuffer === 'number') { - DEFAULT_TTL_BUFFER = cfg.ttlBuffer; - } else { - logError('Invalid value for ttlBuffer', cfg.ttlBuffer); - } -}) const CFG_ALLOW_TARGETING_KEYS = `targetingControls.allowTargetingKeys`; const CFG_ADD_TARGETING_KEYS = `targetingControls.addTargetingKeys`; @@ -47,7 +38,7 @@ export const TARGETING_KEYS = Object.keys(CONSTANTS.TARGETING_KEYS).map( ); // return unexpired bids -const isBidNotExpired = (bid) => (bid.responseTimestamp + (bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : DEFAULT_TTL_BUFFER)) * 1000) > timestamp(); +const isBidNotExpired = (bid) => (bid.responseTimestamp + getTTL(bid) * 1000) > timestamp(); // return bids whose status is not set. Winning bids can only have a status of `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([CONSTANTS.BID_STATUS.RENDERED], bid.status)) || !bid.status); @@ -94,26 +85,26 @@ export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, }); /** -* A descending sort function that will sort the list of objects based on the following two dimensions: -* - bids with a deal are sorted before bids w/o a deal -* - then sort bids in each grouping based on the hb_pb value -* eg: the following list of bids would be sorted like: -* [{ -* "hb_adid": "vwx", -* "hb_pb": "28", -* "hb_deal": "7747" -* }, { -* "hb_adid": "jkl", -* "hb_pb": "10", -* "hb_deal": "9234" -* }, { -* "hb_adid": "stu", -* "hb_pb": "50" -* }, { -* "hb_adid": "def", -* "hb_pb": "2" -* }] -*/ + * A descending sort function that will sort the list of objects based on the following two dimensions: + * - bids with a deal are sorted before bids w/o a deal + * - then sort bids in each grouping based on the hb_pb value + * eg: the following list of bids would be sorted like: + * [{ + * "hb_adid": "vwx", + * "hb_pb": "28", + * "hb_deal": "7747" + * }, { + * "hb_adid": "jkl", + * "hb_pb": "10", + * "hb_deal": "9234" + * }, { + * "hb_adid": "stu", + * "hb_pb": "50" + * }, { + * "hb_adid": "def", + * "hb_pb": "2" + * }] + */ export function sortByDealAndPriceBucketOrCpm(useCpm = false) { return function(a, b) { if (a.adserverTargeting.hb_deal !== undefined && b.adserverTargeting.hb_deal === undefined) { @@ -479,6 +470,12 @@ export function newTargeting(auctionManager) { .filter(bid => deepAccess(bid, 'video.context') !== ADPOD) .filter(isBidUsable); + bidsReceived + .forEach(bid => { + bid.latestTargetedAuctionId = latestAuctionForAdUnit[bid.adUnitCode]; + return bid; + }); + return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); } diff --git a/src/userSync.js b/src/userSync.js index 936836eb12e..153b36816a4 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -244,7 +244,7 @@ export function newUserSync(deps) { * @param {string} type The type of the sync; either image or iframe * @param {string} bidder The name of the adapter. e.g. "rubicon" * @returns {boolean} true => bidder is not allowed to register; false => bidder can register - */ + */ function shouldBidderBeBlocked(type, bidder) { let filterConfig = usConfig.filterSettings; @@ -309,7 +309,7 @@ export function newUserSync(deps) { * @function syncUsers * @summary Trigger all the user syncs based on publisher-defined timeout * @public - * @params {int} timeout The delay in ms before syncing data - default 0 + * @params {number} timeout The delay in ms before syncing data - default 0 */ publicApi.syncUsers = (timeout = 0) => { if (timeout) { @@ -358,7 +358,7 @@ export const userSync = newUserSync(Object.defineProperties({ * * @property {boolean} enableOverride * @property {boolean} syncEnabled - * @property {int} syncsPerBidder + * @property {number} syncsPerBidder * @property {string[]} enabledBidders * @property {Object} filterSettings */ diff --git a/src/utils.js b/src/utils.js index ece29732723..67ed7339675 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ import {config} from './config.js'; import clone from 'just-clone'; -import {find, includes} from './polyfill.js'; +import {includes} from './polyfill.js'; import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; import {getGlobal} from './prebidGlobal.js'; @@ -8,7 +8,6 @@ import {getGlobal} from './prebidGlobal.js'; export { default as deepAccess } from 'dlv/index.js'; export { dset as deepSetValue } from 'dset'; -var tArr = 'Array'; var tStr = 'String'; var tFn = 'Function'; var tNumb = 'Number'; @@ -64,17 +63,6 @@ export function getPrebidInternal() { return prebidInternal; } -var uniqueRef = {}; -export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === uniqueRef - ? Function.prototype.bind - : function(bind) { - var self = this; - var args = Array.prototype.slice.call(arguments, 1); - return function() { - return self.apply(bind, args.concat(Array.prototype.slice.call(arguments))); - }; - }; - /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; @@ -114,19 +102,7 @@ function _getRandomData() { } export function getBidIdParameter(key, paramsObj) { - if (paramsObj && paramsObj[key]) { - return paramsObj[key]; - } - - return ''; -} - -export function tryAppendQueryString(existingUrl, key, value) { - if (value) { - return existingUrl + key + '=' + encodeURIComponent(value) + '&'; - } - - return existingUrl; + return paramsObj?.[key] || ''; } // parse a query string object passed in bid params @@ -145,84 +121,30 @@ export function parseQueryStringParameters(queryObj) { export function transformAdServerTargetingObj(targeting) { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { - return getKeys(targeting) - .map(key => `${key}=${encodeURIComponent(getValue(targeting, key))}`).join('&'); + return Object.keys(targeting) + .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&'); } else { return ''; } } -/** - * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) - * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` - * @param {object} adUnit one adUnit object from the normal list of adUnits - * @returns {Array.} array of arrays containing numeric sizes - */ -export function getAdUnitSizes(adUnit) { - if (!adUnit) { - return; - } - - let sizes = []; - if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { - let bannerSizes = adUnit.mediaTypes.banner.sizes; - if (Array.isArray(bannerSizes[0])) { - sizes = bannerSizes; - } else { - sizes.push(bannerSizes); - } - // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders - } else if (Array.isArray(adUnit.sizes)) { - if (Array.isArray(adUnit.sizes[0])) { - sizes = adUnit.sizes; - } else { - sizes.push(adUnit.sizes); - } - } - return sizes; -} - /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ export function parseSizesInput(sizeObj) { - var parsedSizes = []; - - // if a string for now we can assume it is a single size, like "300x250" if (typeof sizeObj === 'string') { // multiple sizes will be comma-separated - var sizes = sizeObj.split(','); - - // regular expression to match strigns like 300x250 - // start of line, at least 1 number, an "x" , then at least 1 number, and the then end of the line - var sizeRegex = /^(\d)+x(\d)+$/i; - if (sizes) { - for (var curSizePos in sizes) { - if (hasOwn(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { - parsedSizes.push(sizes[curSizePos]); - } - } - } + return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i)) } else if (typeof sizeObj === 'object') { - var sizeArrayLength = sizeObj.length; - - // don't process empty array - if (sizeArrayLength > 0) { - // if we are a 2 item array of 2 numbers, we must be a SingleSize array - if (sizeArrayLength === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj)); - } else { - // otherwise, we must be a MultiSize array - for (var i = 0; i < sizeArrayLength; i++) { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj[i])); - } - } + if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { + return [parseGPTSingleSizeArray(sizeObj)]; + } else { + return sizeObj.map(parseGPTSingleSizeArray) } } - - return parsedSizes; + return []; } // Parse a GPT style single size array, (i.e [300, 250]) @@ -345,6 +267,9 @@ export function createInvisibleIframe() { f.frameBorder = '0'; f.src = 'about:blank'; f.style.display = 'none'; + f.style.height = '0px'; + f.style.width = '0px'; + f.allowtransparency = 'true'; return f; } @@ -375,9 +300,7 @@ export function isStr(object) { return isA(object, tStr); } -export function isArray(object) { - return isA(object, tArr); -} +export const isArray = Array.isArray.bind(Array); export function isNumber(object) { return isA(object, tNumb); @@ -402,12 +325,7 @@ export function isEmpty(object) { if (isArray(object) || isStr(object)) { return !(object.length > 0); } - - for (var k in object) { - if (hasOwnProperty.call(object, k)) return false; - } - - return true; + return Object.keys(object).length <= 0; } /** @@ -426,38 +344,12 @@ export function isEmptyStr(str) { * @param {Function(value, key, object)} fn */ export function _each(object, fn) { - if (isEmpty(object)) return; - if (isFn(object.forEach)) return object.forEach(fn, this); - - var k = 0; - var l = object.length; - - if (l > 0) { - for (; k < l; k++) fn(object[k], k, object); - } else { - for (k in object) { - if (hasOwnProperty.call(object, k)) fn.call(this, object[k], k); - } - } + if (isFn(object?.forEach)) return object.forEach(fn, this); + Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } export function contains(a, obj) { - if (isEmpty(a)) { - return false; - } - - if (isFn(a.indexOf)) { - return a.indexOf(obj) !== -1; - } - - var i = a.length; - while (i--) { - if (a[i] === obj) { - return true; - } - } - - return false; + return isFn(a?.includes) && a.includes(obj); } /** @@ -468,24 +360,10 @@ export function contains(a, obj) { * @return {Array} */ export function _map(object, callback) { - if (isEmpty(object)) return []; - if (isFn(object.map)) return object.map(callback); - var output = []; - _each(object, function (value, key) { - output.push(callback(value, key, object)); - }); - - return output; + if (isFn(object?.map)) return object.map(callback); + return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)) } -export function hasOwn(objectToCheck, propertyToCheckFor) { - if (objectToCheck.hasOwnProperty) { - return objectToCheck.hasOwnProperty(propertyToCheckFor); - } else { - return (typeof objectToCheck[propertyToCheckFor] !== 'undefined') && (objectToCheck.constructor.prototype[propertyToCheckFor] !== objectToCheck[propertyToCheckFor]); - } -}; - /* * Inserts an element(elm) as targets child, by default as first child * @param {HTMLElement} elm @@ -568,27 +446,14 @@ export function insertHtmlIntoIframe(htmlCode) { if (!htmlCode) { return; } - - let iframe = document.createElement('iframe'); - iframe.id = getUniqueIdentifierStr(); - iframe.width = 0; - iframe.height = 0; - iframe.hspace = '0'; - iframe.vspace = '0'; - iframe.marginWidth = '0'; - iframe.marginHeight = '0'; - iframe.style.display = 'none'; - iframe.style.height = '0px'; - iframe.style.width = '0px'; - iframe.scrolling = 'no'; - iframe.frameBorder = '0'; - iframe.allowtransparency = 'true'; - + const iframe = createInvisibleIframe(); internal.insertElement(iframe, document, 'body'); - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(htmlCode); - iframe.contentWindow.document.close(); + ((doc) => { + doc.open(); + doc.write(htmlCode); + doc.close(); + })(iframe.contentWindow.document); } /** @@ -654,19 +519,6 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -export function getValueString(param, val, defaultValue) { - if (val === undefined || val === null) { - return defaultValue; - } - if (isStr(val)) { - return val; - } - if (isNumber(val)) { - return val.toString(); - } - internal.logWarn('Unsuported type for param: ' + param + ' required type: String'); -} - export function uniques(value, index, arry) { return arry.indexOf(value) === index; } @@ -679,38 +531,14 @@ export function getBidRequest(id, bidderRequests) { if (!id) { return; } - let bidRequest; - bidderRequests.some(bidderRequest => { - let result = find(bidderRequest.bids, bid => ['bidId', 'adId', 'bid_id'].some(type => bid[type] === id)); - if (result) { - bidRequest = result; - } - return result; - }); - return bidRequest; -} - -export function getKeys(obj) { - return Object.keys(obj); + return bidderRequests.flatMap(br => br.bids) + .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)) } export function getValue(obj, key) { return obj[key]; } -/** - * Get the key of an object for a given value - */ -export function getKeyByValue(obj, value) { - for (let prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (obj[prop] === value) { - return prop; - } - } - } -} - export function getBidderCodes(adUnits = pbjsInstance.adUnits) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) @@ -729,26 +557,6 @@ export function isApnGetTagDefined() { } } -// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = getHighestCpmCallback('timeToRespond', (previous, current) => previous > current); - -// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 -export const getOldestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous > current); - -// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 -export const getLatestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous < current); - -function getHighestCpmCallback(useTieBreakerProperty, tieBreakerCallback) { - return (previous, current) => { - if (previous.cpm === current.cpm) { - return tieBreakerCallback(previous[useTieBreakerProperty], current[useTieBreakerProperty]) ? current : previous; - } - return previous.cpm < current.cpm ? current : previous; - } -} - /** * Fisher–Yates shuffle * http://stackoverflow.com/a/6274398 @@ -775,10 +583,6 @@ export function shuffle(array) { return array; } -export function adUnitsFilter(filter, bid) { - return includes(filter, bid && bid.adUnitCode); -} - export function deepClone(obj) { return clone(obj); } @@ -795,9 +599,15 @@ export function isSafariBrowser() { return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } -export function replaceAuctionPrice(str, cpm) { +export function replaceMacros(str, subs) { if (!str) return; - return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); + return Object.entries(subs).reduce((str, [key, val]) => { + return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || ''); + }, str); +} + +export function replaceAuctionPrice(str, cpm) { + return replaceMacros(str, {AUCTION_PRICE: cpm}) } export function replaceClickThrough(str, clicktag) { @@ -843,7 +653,7 @@ export function checkCookieSupport() { * * @param {function} func The function which should be executed, once the returned function has been executed * numRequiredCalls times. - * @param {int} numRequiredCalls The number of times which the returned function needs to be called before + * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ export function delayExecution(func, numRequiredCalls) { @@ -862,7 +672,7 @@ export function delayExecution(func, numRequiredCalls) { /** * https://stackoverflow.com/a/34890276/428704 * @export - * @param {array} xs + * @param {Array} xs * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ @@ -925,8 +735,7 @@ export function isValidMediaTypes(mediaTypes) { export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { return adUnits .filter(adUnit => adUnit.code === adUnitCode) - .map((adUnit) => adUnit.bids) - .reduce(flatten, []) + .flatMap((adUnit) => adUnit.bids) .filter((bidderData) => bidderData.bidder === bidder) .map((bidderData) => bidderData.params || {}); } @@ -938,7 +747,7 @@ export function getDNT() { return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes'; } -const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; +export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot @@ -949,41 +758,6 @@ export function isAdUnitCodeMatchingSlot(slot) { return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); } -/** - * Returns filter function to match adUnitCode in slot - * @param {string} adUnitCode AdUnit code - * @return {function} filter function - */ -export function isSlotMatchingAdUnitCode(adUnitCode) { - return (slot) => compareCodeAndSlot(slot, adUnitCode); -} - -/** - * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page - */ -export function getGptSlotForAdUnitCode(adUnitCode) { - let matchingSlot; - if (isGptPubadsDefined()) { - // find the first matching gpt slot on the page - matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); - } - return matchingSlot; -}; - -/** - * @summary Uses the adUnit's code in order to find a matching gptSlot on the page - */ -export function getGptSlotInfoForAdUnitCode(adUnitCode) { - const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); - if (matchingSlot) { - return { - gptSlot: matchingSlot.getAdUnitPath(), - divId: matchingSlot.getSlotElementId() - } - } - return {}; -}; - /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped @@ -1005,33 +779,14 @@ export function unsupportedBidderMessage(adUnit, bidder) { * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger * @param {*} value */ -export function isInteger(value) { - if (Number.isInteger) { - return Number.isInteger(value); - } else { - return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; - } -} - -/** - * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' - * @param {string} value string value to convert - */ -export function convertCamelToUnderscore(value) { - return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { return '_' + y.toLowerCase() }).replace(/^_/, ''); -} +export const isInteger = Number.isInteger.bind(Number); /** * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ export function cleanObj(obj) { - return Object.keys(obj).reduce((newObj, key) => { - if (typeof obj[key] !== 'undefined') { - newObj[key] = obj[key]; - } - return newObj; - }, {}) + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')) } /** @@ -1068,104 +823,10 @@ export function pick(obj, properties) { }, {}); } -/** - * Try to convert a value to a type. - * If it can't be done, the value will be returned. - * - * @param {string} typeToConvert The target type. e.g. "string", "number", etc. - * @param {*} value The value to be converted into typeToConvert. - */ -function tryConvertType(typeToConvert, value) { - if (typeToConvert === 'string') { - return value && value.toString(); - } else if (typeToConvert === 'number') { - return Number(value); - } else { - return value; - } -} - -export function convertTypes(types, params) { - Object.keys(types).forEach(key => { - if (params[key]) { - if (isFn(types[key])) { - params[key] = types[key](params[key]); - } else { - params[key] = tryConvertType(types[key], params[key]); - } - - // don't send invalid values - if (isNaN(params[key])) { - delete params.key; - } - } - }); - return params; -} - export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } -/** - * Creates an array of n length and fills each item with the given value - */ -export function fill(value, length) { - let newArray = []; - - for (let i = 0; i < length; i++) { - let valueToPush = isPlainObject(value) ? deepClone(value) : value; - newArray.push(valueToPush); - } - - return newArray; -} - -/** - * http://npm.im/chunk - * Returns an array with *size* chunks from given array - * - * Example: - * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => - * [['a', 'b'], ['c', 'd'], ['e']] - */ -export function chunk(array, size) { - let newArray = []; - - for (let i = 0; i < Math.ceil(array.length / size); i++) { - let start = i * size; - let end = start + size; - newArray.push(array.slice(start, end)); - } - - return newArray; -} - -export function getMinValueFromArray(array) { - return Math.min(...array); -} - -export function getMaxValueFromArray(array) { - return Math.max(...array); -} - -/** - * This function will create compare function to sort on object property - * @param {string} property - * @returns {function} compare function to be used in sorting - */ -export function compareOn(property) { - return function compare(a, b) { - if (a[property] < b[property]) { - return 1; - } - if (a[property] > b[property]) { - return -1; - } - return 0; - } -} - export function parseQS(query) { return !query ? {} : query .replace(/^\?/, '') @@ -1328,15 +989,6 @@ export function cyrb53Hash(str, seed = 0) { return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); } -/** - * returns a window object, which holds the provided document or null - * @param {Document} doc - * @returns {Window} - */ -export function getWindowFromDocument(doc) { - return (doc) ? doc.defaultView : null; -} - /** * returns the result of `JSON.parse(data)`, or undefined if that throws an error. * @param data @@ -1375,36 +1027,33 @@ export function memoize(fn, key = function (arg) { return arg; }) { * @param {object} attributes */ export function setScriptAttributes(script, attributes) { - for (let key in attributes) { - if (attributes.hasOwnProperty(key)) { - script.setAttribute(key, attributes[key]); - } - } + Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)) } /** - * Encode a string for inclusion in HTML. - * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and - * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ - * @return {string} + * Perform a binary search for `el` on an ordered array `arr`. + * + * @returns the lowest nonnegative integer I that satisfies: + * key(arr[i]) >= key(el) for each i between I and arr.length + * + * (if one or more matches are found for `el`, returns the index of the first; + * if the element is not found, return the index of the first element that's greater; + * if no greater element exists, return `arr.length`) */ -export const escapeUnsafeChars = (() => { - const escapes = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', - '\0': '\\0', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - - return function(str) { - return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]) +export function binarySearch(arr, el, key = (el) => el) { + let left = 0; + let right = arr.length && arr.length - 1; + const target = key(el); + while (right - left > 1) { + const middle = left + Math.round((right - left) / 2); + if (target > key(arr[middle])) { + left = middle; + } else { + right = middle; + } } -})(); + while (arr.length > left && target > key(arr[left])) { + left++; + } + return left; +} diff --git a/src/utils/currency.js b/src/utils/currency.js deleted file mode 100644 index ab7e5faa1ea..00000000000 --- a/src/utils/currency.js +++ /dev/null @@ -1,16 +0,0 @@ -import {getGlobal} from '../prebidGlobal.js'; - -/** - * "best effort" wrapper around currency conversion; always returns an amount that may or may not be correct. - */ -export function beConvertCurrency(amount, from, to) { - if (from === to) return amount; - let result = amount; - if (typeof getGlobal().convertCurrency === 'function') { - try { - result = getGlobal().convertCurrency(amount, from, to); - } catch (e) { - } - } - return result; -} diff --git a/src/utils/reducers.js b/src/utils/reducers.js new file mode 100644 index 00000000000..28851be8aaa --- /dev/null +++ b/src/utils/reducers.js @@ -0,0 +1,44 @@ +export function simpleCompare(a, b) { + if (a === b) return 0; + return a < b ? -1 : 1; +} + +export function keyCompare(key = (item) => item) { + return (a, b) => simpleCompare(key(a), key(b)) +} + +export function reverseCompare(compare = simpleCompare) { + return (a, b) => -compare(a, b) || 0; +} + +export function tiebreakCompare(...compares) { + return function (a, b) { + for (const cmp of compares) { + const val = cmp(a, b); + if (val !== 0) return val; + } + return 0; + } +} + +export function minimum(compare = simpleCompare) { + return (min, item) => compare(item, min) < 0 ? item : min; +} + +export function maximum(compare = simpleCompare) { + return minimum(reverseCompare(compare)); +} + +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); + +// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))) + +// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 +export const getOldestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, reverseCompare(timestampCompare))) + +// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 +export const getLatestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, timestampCompare)) diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js new file mode 100644 index 00000000000..392ed1c9ad7 --- /dev/null +++ b/src/utils/ttlCollection.js @@ -0,0 +1,139 @@ +import {GreedyPromise} from './promise.js'; +import {binarySearch, timestamp} from '../utils.js'; + +/** + * Create a set-like collection that automatically forgets items after a certain time. + * + * @param {({}) => Number|Promise} startTime? a function taking an item added to this collection, + * and returning (a promise to) a timestamp to be used as the starting time for the item + * (the item will be dropped after `ttl(item)` milliseconds have elapsed since this timestamp). + * Defaults to the time the item was added to the collection. + * @param {({}) => Number|void|Promise} ttl a function taking an item added to this collection, + * and returning (a promise to) the duration (in milliseconds) the item should be kept in it. + * May return null to indicate that the item should be persisted indefinitely. + * @param {boolean} monotonic? set to true for better performance, but only if, given any two items A and B in this collection: + * if A was added before B, then: + * - startTime(A) + ttl(A) <= startTime(B) + ttl(B) + * - Promise.all([startTime(A), ttl(A)]) never resolves later than Promise.all([startTime(B), ttl(B)]) + * @param {number} slack? maximum duration (in milliseconds) that an item is allowed to persist + * once past its TTL. This is also roughly the interval between "garbage collection" sweeps. + */ +export function ttlCollection( + { + startTime = timestamp, + ttl = () => null, + monotonic = false, + slack = 5000 + } = {} +) { + const items = new Map(); + const pendingPurge = []; + const markForPurge = monotonic + ? (entry) => pendingPurge.push(entry) + : (entry) => pendingPurge.splice(binarySearch(pendingPurge, entry, (el) => el.expiry), 0, entry) + let nextPurge, task; + + function reschedulePurge() { + task && clearTimeout(task); + if (pendingPurge.length > 0) { + const now = timestamp(); + nextPurge = Math.max(now, pendingPurge[0].expiry + slack); + task = setTimeout(() => { + const now = timestamp(); + let cnt = 0; + for (const entry of pendingPurge) { + if (entry.expiry > now) break; + items.delete(entry.item) + cnt++; + } + pendingPurge.splice(0, cnt); + task = null; + reschedulePurge(); + }, nextPurge - now); + } else { + task = null; + } + } + + function mkEntry(item) { + const values = {}; + const thisCohort = currentCohort; + let expiry; + + function update() { + if (thisCohort === currentCohort && values.start != null && values.delta != null) { + expiry = values.start + values.delta; + markForPurge(entry); + if (task == null || nextPurge > expiry + slack) { + reschedulePurge(); + } + } + } + + const [init, refresh] = Object.entries({ + start: startTime, + delta: ttl + }).map(([field, getter]) => { + let currentCall; + return function() { + const thisCall = currentCall = {}; + GreedyPromise.resolve(getter(item)).then((val) => { + if (thisCall === currentCall) { + values[field] = val; + update(); + } + }); + } + }) + + const entry = { + item, + refresh, + get expiry() { + return expiry; + }, + }; + + init(); + refresh(); + return entry; + } + + let currentCohort = {}; + + return { + [Symbol.iterator]: () => items.keys(), + /** + * Add an item to this collection. + * @param item + */ + add(item) { + !items.has(item) && items.set(item, mkEntry(item)); + }, + /** + * Clear this collection. + */ + clear() { + pendingPurge.length = 0; + reschedulePurge(); + items.clear(); + currentCohort = {}; + }, + /** + * @returns {[]} all the items in this collection, in insertion order. + */ + toArray() { + return Array.from(items.keys()); + }, + /** + * Refresh the TTL for each item in this collection. + */ + refresh() { + pendingPurge.length = 0; + reschedulePurge(); + for (const entry of items.values()) { + entry.refresh(); + } + }, + }; +} diff --git a/src/video.js b/src/video.js index 7930e318874..ff137892a2b 100644 --- a/src/video.js +++ b/src/video.js @@ -1,25 +1,21 @@ -import adapterManager from './adapterManager.js'; -import { deepAccess, logError } from './utils.js'; -import { config } from '../src/config.js'; -import {includes} from './polyfill.js'; -import { hook } from './hook.js'; +import {deepAccess, logError} from './utils.js'; +import {config} from '../src/config.js'; +import {hook} from './hook.js'; import {auctionManager} from './auctionManager.js'; -const VIDEO_MEDIA_TYPE = 'video'; export const OUTSTREAM = 'outstream'; export const INSTREAM = 'instream'; -/** - * Helper functions for working with video-enabled adUnits - */ -export const videoAdUnit = adUnit => { - const mediaType = adUnit.mediaType === VIDEO_MEDIA_TYPE; - const mediaTypes = deepAccess(adUnit, 'mediaTypes.video'); - return mediaType || mediaTypes; -}; -export const videoBidder = bid => includes(adapterManager.videoAdapters, bid.bidder); -export const hasNonVideoBidder = adUnit => - adUnit.bids.filter(bid => !videoBidder(bid)).length; +export function fillVideoDefaults(adUnit) { + const video = adUnit?.mediaTypes?.video; + if (video != null && video.plcmt == null) { + if (video.context === OUTSTREAM || [2, 3, 4].includes(video.placement)) { + video.plcmt = 4; + } else if (video.context !== OUTSTREAM && [2, 6].includes(video.playbackmethod)) { + video.plcmt = 2; + } + } +} /** * @typedef {object} VideoBid diff --git a/test/spec/appnexusKeywords_spec.js b/test/spec/appnexusKeywords_spec.js index 9bf567a27c5..68faeff0b82 100644 --- a/test/spec/appnexusKeywords_spec.js +++ b/test/spec/appnexusKeywords_spec.js @@ -1,4 +1,4 @@ -import {transformBidderParamKeywords} from '../../libraries/appnexusKeywords/anKeywords.js'; +import {transformBidderParamKeywords} from '../../libraries/appnexusUtils/anKeywords.js'; import {expect} from 'chai/index.js'; import * as utils from '../../src/utils.js'; diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 4061e757d97..137d009bd18 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -5,7 +5,7 @@ import { adjustBids, getMediaTypeGranularity, getPriceByGranularity, - addBidResponse + addBidResponse, resetAuctionState, responsesReady } from 'src/auction.js'; import CONSTANTS from 'src/constants.json'; import * as auctionModule from 'src/auction.js'; @@ -23,6 +23,7 @@ import {AuctionIndex} from '../../src/auctionIndex.js'; import {expect} from 'chai'; import {deepClone} from '../../src/utils.js'; import { IMAGE as ortbNativeRequest } from 'src/native.js'; +import {PrebidServer} from '../../modules/prebidServerBidAdapter/index.js'; var assert = require('assert'); @@ -48,6 +49,7 @@ function mockBid(opts) { let bidderCode = opts && opts.bidderCode; return { + adUnitCode: opts?.adUnitCode || ADUNIT_CODE, 'ad': 'creative', 'cpm': '1.99', 'width': 300, @@ -68,6 +70,11 @@ function mockBid(opts) { transactionId: this.transactionId, auctionId: this.auctionId } + }, + _ctx: { + adUnits: opts?.adUnits, + src: opts?.src, + uniquePbsTid: opts?.uniquePbsTid, } }; } @@ -96,6 +103,9 @@ function mockBidRequest(bid, opts) { 'bidderCode': bidderCode || bid.bidderCode, 'auctionId': opts && opts.auctionId, 'bidderRequestId': requestId, + src: bid?._ctx?.src, + adUnitsS2SCopy: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.adUnits : undefined, + uniquePbsTid: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.uniquePbsTid : undefined, 'bids': [ { 'bidder': bidderCode || bid.bidderCode, @@ -108,7 +118,8 @@ function mockBidRequest(bid, opts) { 'bidId': bid.requestId, 'bidderRequestId': requestId, 'auctionId': opts && opts.auctionId, - 'mediaTypes': mediaType + 'mediaTypes': mediaType, + src: bid?._ctx?.src } ], 'auctionStart': 1505250713622, @@ -160,6 +171,7 @@ describe('auctionmanager.js', function () { indexAuctions = []; indexStub = sinon.stub(auctionManager, 'index'); indexStub.get(() => new AuctionIndex(() => indexAuctions)); + resetAuctionState(); }); afterEach(() => { @@ -515,7 +527,7 @@ describe('auctionmanager.js', function () { s2sConfig: { accountId: '1', enabled: true, - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders: ['appnexus'], timeout: 1000, adapter: 'prebidServer' @@ -763,9 +775,10 @@ describe('auctionmanager.js', function () { }); describe('createAuction', () => { - let adUnits, stubMakeBidRequests, stubCallAdapters + let adUnits, stubMakeBidRequests, stubCallAdapters, bids; beforeEach(() => { + bids = []; stubMakeBidRequests = sinon.stub(adapterManager, 'makeBidRequests').returns([{ bidderCode: BIDDER_CODE, bids: [{ @@ -773,6 +786,7 @@ describe('auctionmanager.js', function () { }] }]); stubCallAdapters = sinon.stub(adapterManager, 'callBids').callsFake((au, reqs, addBid, done) => { + bids.forEach(bid => addBid(bid.adUnitCode, bid)); reqs.forEach(r => done.apply(r)); }); adUnits = [{ @@ -787,6 +801,7 @@ describe('auctionmanager.js', function () { afterEach(() => { stubMakeBidRequests.restore(); stubCallAdapters.restore(); + auctionManager.clearAllAuctions(); }); it('passes global and bidder ortb2 to the auction', () => { @@ -814,6 +829,79 @@ describe('auctionmanager.js', function () { }); expect(auction.getNonBids()[0]).to.equal('test'); }); + + describe('stale auctions', () => { + let clock, auction; + beforeEach(() => { + clock = sinon.useFakeTimers(); + auction = auctionManager.createAuction({adUnits}); + indexAuctions.push(auction); + }); + afterEach(() => { + clock.restore(); + config.resetConfig(); + }); + + it('are dropped after their last bid becomes stale (if minBidCacheTTL is set)', () => { + config.setConfig({ + minBidCacheTTL: 0 + }); + bids = [ + { + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 10 + }, { + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 100 + } + ]; + auction.callBids(); + return auction.end.then(() => { + clock.tick(50 * 1000); + expect(auctionManager.getBidsReceived().length).to.equal(2); + clock.tick(56 * 1000); + expect(auctionManager.getBidsReceived()).to.eql([]); + }); + }); + + it('are dropped after `minBidCacheTTL` seconds if they had no bid', () => { + auction.callBids(); + config.setConfig({ + minBidCacheTTL: 2 + }); + return auction.end.then(() => { + expect(auctionManager.getNoBids().length).to.eql(1); + clock.tick(10 * 10000); + expect(auctionManager.getNoBids().length).to.eql(0); + }) + }); + + Object.entries({ + 'bids': { + bd: [{ + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 10 + }], + entries: () => auctionManager.getBidsReceived() + }, + 'no bids': { + bd: [], + entries: () => auctionManager.getNoBids() + } + }).forEach(([t, {bd, entries}]) => { + it(`with ${t} are never dropped if minBidCacheTTL is not set`, () => { + bids = bd; + auction.callBids(); + return auction.end.then(() => { + clock.tick(100 * 1000); + expect(entries().length > 0).to.be.true; + }) + }) + }); + }) }); describe('addBidResponse #1', function () { @@ -839,6 +927,11 @@ describe('auctionmanager.js', function () { beforeEach(function () { ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockAjaxBuilder); adUnits = [{ + mediaTypes: { + banner: { + sizes: [] + } + }, code: ADUNIT_CODE, transactionId: ADUNIT_CODE, bids: [ @@ -1024,36 +1117,47 @@ describe('auctionmanager.js', function () { assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); }); - it('bid for a regular unit and a video unit', function() { - let renderer = { - url: 'renderer.js', - render: (bid) => bid - }; - Object.assign(adUnits[0], {renderer}); - // make sure that if the renderer is only on the second ad unit, prebid - // still correctly uses it - let bid = mockBid(); - let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; - - bidRequests[0].bids[1] = Object.assign({ - bidId: utils.getUniqueIdentifierStr() - }, bidRequests[0].bids[0]); - Object.assign(bidRequests[0].bids[0], { - adUnitCode: ADUNIT_CODE1, - transactionId: ADUNIT_CODE1, - }); + describe('bid for a regular unit and a video unit', () => { + beforeEach(() => { + const renderer = { + url: 'renderer.js', + render: (bid) => bid + }; + Object.assign(adUnits[0], {renderer}); + // make sure that if the renderer is only on the second ad unit, prebid + // still correctly uses it + let bid = mockBid(); + let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; + + bidRequests[0].bids[1] = Object.assign({ + bidId: utils.getUniqueIdentifierStr() + }, bidRequests[0].bids[0]); + Object.assign(bidRequests[0].bids[0], { + adUnitCode: ADUNIT_CODE1, + transactionId: ADUNIT_CODE1, + }); - makeRequestsStub.returns(bidRequests); + makeRequestsStub.returns(bidRequests); - // this should correspond with the second bid in the bidReq because of the ad unit code - bid.mediaType = 'video-outstream'; - spec.interpretResponse.returns(bid); + // this should correspond with the second bid in the bidReq because of the ad unit code + bid.mediaType = 'video-outstream'; + spec.interpretResponse.returns(bid); + }); - auction.callBids(); + it('should use renderers on bid response', () => { + auction.callBids(); - const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode == ADUNIT_CODE); - assert.equal(addedBid.renderer.url, 'renderer.js'); - }); + const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode === ADUNIT_CODE); + assert.equal(addedBid.renderer.url, 'renderer.js'); + }); + + it('should resolve .end', () => { + auction.callBids(); + return auction.end.then(() => { + expect(auction.getBidsReceived().length).to.eql(1); + }) + }); + }) it('sets bidResponse.ttlBuffer from adUnit.ttlBuffer', () => { adUnits[0].ttlBuffer = 0; @@ -1063,12 +1167,19 @@ describe('auctionmanager.js', function () { }); describe('when auction timeout is 20', function () { - let eventsEmitSpy; + let eventsEmitSpy, auctionDone; - function setupBids(auctionId) { - bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; - let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId})); + function respondToRequest(requestIndex) { + server.requests[requestIndex].respond(200, {}, 'response body'); + } + + function runAuction() { + let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId: auction.getAuctionId()})); makeRequestsStub.returns(bidRequests); + return new Promise((resolve) => { + auctionDone = resolve; + auction.callBids(); + }) } beforeEach(function () { @@ -1077,86 +1188,156 @@ describe('auctionmanager.js', function () { transactionId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, + {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, ] }]; adUnitCodes = [ADUNIT_CODE]; eventsEmitSpy = sinon.spy(events, 'emit'); + bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; + const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); + registerBidder(spec1); + const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); + registerBidder(spec2); + auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: () => auctionDone(), cbTimeout: 20}); + indexAuctions = [auction]; }); + afterEach(function () { events.emit.restore(); }); - it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); + it('resolves .end on timeout', () => { + let endResolved = false; + auction.end.then(() => { + endResolved = true; + }) + const pm = runAuction().then(() => { + expect(endResolved).to.be.true; + }); + respondToRequest(0); + return pm; + }); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + describe('AUCTION_TIMEOUT event', () => { + let handler; + beforeEach(() => { + handler = sinon.spy(); + events.on(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }) + afterEach(() => { + events.off(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }); + + Object.entries({ + 'is fired on timeout': [true, [0]], + 'is NOT fired otherwise': [false, [0, 1]], + }).forEach(([t, [shouldFire, respond]]) => { + it(t, () => { + const pm = runAuction().then(() => { + if (shouldFire) { + sinon.assert.calledWith(handler, sinon.match({auctionId: auction.getAuctionId()})) + } else { + sinon.assert.notCalled(handler); + } + }); + respond.forEach(respondToRequest); + return pm; + }) + }); + }); + + it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); // Check that additional properties are available - assert.equal(timedOutBids[0].params.placementId, 'id'); + assert.equal(timedOutBids[0].params[0].placementId, 'id'); const auctionEndCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.AUCTION_END).getCalls()[0]; const auctionProps = auctionEndCall.args[1]; assert.equal(auctionProps.adUnits, adUnits); assert.equal(auctionProps.timeout, 20); assert.equal(auctionProps.auctionStatus, AUCTION_COMPLETED) - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - - auction.callBids(); + }); respondToRequest(0); + return pm; }); - it('should NOT emit BID_TIMEOUT when all bidders responded in time', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT when all bidders responded in time', function () { + const pm = runAuction().then(() => { assert.ok(eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).notCalled, 'did not emit event BID_TIMEOUT'); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); respondToRequest(1); + return pm; }); - it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function (done) { - const spec1 = mockBidder(BIDDER_CODE, []); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, []); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); + return pm; }); + + it('should NOT emit BID_TIMEOUT for bidders that replied through S2S', () => { + adapterManager.registerBidAdapter(new PrebidServer(), 'pbs'); + config.setConfig({ + s2sConfig: [{ + accountId: '1', + enabled: true, + defaultVendor: 'appnexuspsp', + bidders: ['mock-s2s-1'], + adapter: 'pbs' + }, { + accountId: '1', + enabled: true, + defaultVendor: 'rubicon', + bidders: ['mock-s2s-2'], + adapter: 'pbs' + }] + }) + adUnits[0].bids.push({bidder: 'mock-s2s-1'}, {bidder: 'mock-s2s-2'}) + const s2sAdUnits = deepClone(adUnits); + bids.unshift( + mockBid({bidderCode: 'mock-s2s-1', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '1'}), + mockBid({bidderCode: 'mock-s2s-2', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '2'}) + ); + Object.assign(s2sAdUnits[0], { + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [ + { + bidder: 'mock-s2s-1', + bid_id: bids[0].requestId + }, + { + bidder: 'mock-s2s-2', + bid_id: bids[1].requestId + } + ] + }) + + const pm = runAuction().then(() => { + const toBids = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0].args[1] + expect(toBids.map(bid => bid.bidder)).to.eql([ + 'mock-s2s-2', + BIDDER_CODE, + BIDDER_CODE1, + ]) + }); + respondToRequest(1); + return pm; + }) }); }); @@ -1651,116 +1832,54 @@ describe('auctionmanager.js', function () { sinon.assert.calledWith(auction.addBidReceived, sinon.match({cpm: 1.23})); }) - describe('when addBidResponse hook returns promises', () => { - let resolvers, callbacks, bids; + describe('when responsesReady defers', () => { + let resolve, reject, promise, callbacks, bids; - function hook(next, ...args) { - next.bail(new Promise((resolve, reject) => { - resolvers.resolve.push(resolve); - resolvers.reject.push(reject); - }).finally(() => next(...args))); + function hook(next, ready) { + next(ready.then(() => promise)); } - function invokeCallbacks() { - bids.forEach((bid) => callbacks.addBidResponse(ADUNIT_CODE, bid)); - bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); - } + before(() => { + responsesReady.before(hook); + }); - function delay(ms = 0) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }); - } + after(() => { + responsesReady.getHooks({hook}).remove(); + }); beforeEach(() => { + // eslint-disable-next-line promise/param-names + promise = new Promise((rs, rj) => { + resolve = rs; + reject = rj; + }); bids = [ mockBid({bidderCode: BIDDER_CODE1}), mockBid({bidderCode: BIDDER_CODE}) ] bidRequests = bids.map((b) => mockBidRequest(b)); - resolvers = {resolve: [], reject: []}; - addBidResponse.before(hook); callbacks = auctionCallbacks(doneSpy, auction); Object.assign(auction, { addNoBid: sinon.spy() }); }); - afterEach(() => { - addBidResponse.getHooks({hook: hook}).remove(); - }); - - it('should wait for bids without a request bids before calling auctionDone', () => { - callbacks.addBidResponse(ADUNIT_CODE, Object.assign(mockBid(), {requestId: null})); - invokeCallbacks(); - resolvers.resolve.slice(1, 3).forEach((fn) => fn()); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - resolvers.resolve[0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - Object.entries({ - 'all succeed': ['resolve', 'resolve'], - 'some fail': ['resolve', 'reject'], - 'all fail': ['reject', 'reject'] - }).forEach(([test, results]) => { - describe(`(and ${test})`, () => { - it('should wait for them to complete before calling auctionDone', () => { - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[0]][0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[1]][1](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); + 'resolve': () => resolve(), + 'reject': () => reject(), + }).forEach(([t, resolver]) => { + it(`should wait for responsesReady to ${t} before calling auctionDone`, (done) => { + bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); + setTimeout(() => { + sinon.assert.notCalled(doneSpy); + resolver(); + setTimeout(() => { + sinon.assert.called(doneSpy); + done(); + }) + }) }); }); - - Object.entries({ - bidder: (timeout) => { - bidRequests.forEach((r) => r.timeout = timeout); - auction.getTimeout = () => timeout + 10000 - }, - auction: (timeout) => { - auction.getTimeout = () => timeout; - bidRequests.forEach((r) => r.timeout = timeout + 10000) - } - }).forEach(([test, setTimeout]) => { - it(`should respect ${test} timeout if they never complete`, () => { - const start = Date.now() - 2900; - auction.getAuctionStart = () => start; - setTimeout(3000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - return delay(100); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - - it(`should not wait if ${test} has already timed out`, () => { - const start = Date.now() - 2000; - auction.getAuctionStart = () => start; - setTimeout(1000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - }) }); describe('when bids are rejected', () => { diff --git a/test/spec/creative/crossDomainCreative_spec.js b/test/spec/creative/crossDomainCreative_spec.js new file mode 100644 index 00000000000..765d5e5311a --- /dev/null +++ b/test/spec/creative/crossDomainCreative_spec.js @@ -0,0 +1,182 @@ +import {renderer} from '../../../libraries/creativeRender/crossDomain.js'; +import { + AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, EXCEPTION, NO_AD, + PREBID_EVENT, + PREBID_REQUEST, + PREBID_RESPONSE +} from '../../../libraries/creativeRender/constants.js'; + +describe('cross-domain creative', () => { + let win, renderAd, messages, mkIframe, listeners; + + beforeEach(() => { + messages = []; + listeners = []; + mkIframe = sinon.stub(); + win = { + document: { + body: { + appendChild: sinon.stub(), + }, + createElement: sinon.stub().callsFake(tagname => { + switch (tagname.toLowerCase()) { + case 'a': + return document.createElement('a') + case 'iframe': { + return mkIframe(); + } + } + }) + }, + addEventListener: sinon.stub().callsFake((_, listener) => listeners.push(listener)), + parent: { + postMessage: sinon.stub().callsFake((payload, targetOrigin, transfer) => { + messages.push({payload: JSON.parse(payload), targetOrigin, transfer}); + }) + } + }; + renderAd = renderer(win); + }) + + it('derives postMessage target origin from pubUrl ', () => { + renderAd({pubUrl: 'https://domain.com:123/path'}); + expect(messages[0].targetOrigin).to.eql('https://domain.com:123') + }); + + it('generates request message with adId and clickUrl', () => { + renderAd({adId: '123', clickUrl: 'https://click-url.com'}); + expect(messages[0].payload).to.eql({ + message: PREBID_REQUEST, + adId: '123', + options: { + clickUrl: 'https://click-url.com' + } + }) + }) + + 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)) + }; + + it('ignores messages that are not a prebid response message', () => { + renderAd({adId: '123'}); + 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('on missing ad', (done) => { + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123'}); + setTimeout(() => { + sinon.assert.match(messages[1].payload, { + message: PREBID_EVENT, + adId: '123', + event: AD_RENDER_FAILED, + info: { + reason: NO_AD, + } + }) + 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('renders adUrl as iframe src', (done) => { + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123', adUrl: 'some-url'}); + setTimeout(() => { + sinon.assert.calledWith(win.document.body.appendChild, iframe); + expect(iframe.attrs.src).to.eql('some-url'); + done(); + }, 100) + }); + + it('renders ad through document.write', (done) => { + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123', ad: 'some-markup'}); + 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(); + }, 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 + }) + }) + }) + }) + }); + }); + }); +}); diff --git a/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js b/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js index 5b5ea2ef2cd..d1803c9784a 100644 --- a/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js +++ b/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js @@ -1,4 +1,4 @@ -/** +/* TODO: old CMP no longer works; see if we can fix this with https://github.com/prebid/Prebid.js/issues/6377 const expect = require('chai').expect; const { testPageURL, switchFrame, waitForElement } = require('../../../helpers/testing-utils'); @@ -59,4 +59,4 @@ describe('Prebid.js GDPR Ad Unit Test', function () { expect(ele.isExisting()).to.be.true; }); }); -**/ + */ diff --git a/test/spec/fpd/enrichment_spec.js b/test/spec/fpd/enrichment_spec.js index 3b3afb15f8c..40692360dca 100644 --- a/test/spec/fpd/enrichment_spec.js +++ b/test/spec/fpd/enrichment_spec.js @@ -3,7 +3,10 @@ import {hook} from '../../../src/hook.js'; import {expect} from 'chai/index.mjs'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; +import * as activities from 'src/activities/rules.js' import {CLIENT_SECTIONS} from '../../../src/fpd/oneClient.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../../../src/activities/activities.js'; +import {ACTIVITY_PARAM_COMPONENT} from '../../../src/activities/params.js'; describe('FPD enrichment', () => { let sandbox; @@ -213,7 +216,7 @@ describe('FPD enrichment', () => { ua: 'ua' }) }) - }) + }); }); }); @@ -310,6 +313,71 @@ describe('FPD enrichment', () => { }); }); + describe('privacy sandbox cookieDeprecationLabel', () => { + let isAllowed, cdep, shouldCleanupNav = false; + + before(() => { + if (!navigator.cookieDeprecationLabel) { + navigator.cookieDeprecationLabel = {}; + shouldCleanupNav = true; + } + }); + + after(() => { + if (shouldCleanupNav) { + delete navigator.cookieDeprecationLabel; + } + }); + + beforeEach(() => { + isAllowed = true; + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_ACCESS_DEVICE && params[ACTIVITY_PARAM_COMPONENT] === 'prebid.cdep') { + return isAllowed; + } else { + throw new Error('Unexpected activity check'); + } + }); + sandbox.stub(window.navigator, 'cookieDeprecationLabel').value({ + getValue: sinon.stub().callsFake(() => cdep) + }) + }) + + it('enrichment sets device.ext.cdep when allowed and navigator.getCookieDeprecationLabel exists', () => { + cdep = Promise.resolve('example-test-label'); + return fpd().then(ortb2 => { + expect(ortb2.device.ext.cdep).to.eql('example-test-label'); + }) + }); + + Object.entries({ + 'not allowed'() { + isAllowed = false; + }, + 'not supported'() { + delete navigator.cookieDeprecationLabel + } + }).forEach(([t, setup]) => { + it(`if ${t}, the navigator API is not called and no enrichment happens`, () => { + setup(); + cdep = Promise.resolve('example-test-label'); + return fpd().then(ortb2 => { + expect(ortb2.device.ext).to.not.exist; + if (navigator.cookieDeprecationLabel) { + sinon.assert.notCalled(navigator.cookieDeprecationLabel.getValue); + } + }) + }); + }) + + it('if the navigator API returns a promise that rejects, the enrichment does not halt forever', () => { + cdep = Promise.reject(new Error('oops, something went wrong')); + return fpd().then(ortb2 => { + expect(ortb2.device.ext).to.not.exist; + }) + }); + }); + it('leaves only one of app, site, dooh', () => { return fpd({ app: {p: 'val'}, diff --git a/test/spec/libraries/currencyUtils_spec.js b/test/spec/libraries/currencyUtils_spec.js new file mode 100644 index 00000000000..9d3d73e6a5f --- /dev/null +++ b/test/spec/libraries/currencyUtils_spec.js @@ -0,0 +1,113 @@ +import {getGlobal} from 'src/prebidGlobal.js'; +import {convertCurrency, currencyCompare, currencyNormalizer} from 'libraries/currencyUtils/currency.js'; + +describe('currency utils', () => { + let sandbox; + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }) + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('convertCurrency', () => { + Object.entries({ + 'not available': () => sandbox.stub(getGlobal(), 'convertCurrency').value(undefined), + 'throwing errors': () => sandbox.stub(getGlobal(), 'convertCurrency').callsFake(() => { throw new Error(); }), + }).forEach(([t, setup]) => { + describe(`when currency module is ${t}`, () => { + beforeEach(setup); + + it('should "convert" to the same currency', () => { + expect(convertCurrency(123, 'mock', 'mock', false)).to.eql(123); + }); + + it('should throw when suppressErrors = false', () => { + expect(() => convertCurrency(123, 'c1', 'c2', false)).to.throw(); + }); + + it('should return input value when suppressErrors = true', () => { + expect(convertCurrency(123, 'c1', 'c2', true)).to.eql(123); + }) + }) + }); + + describe('when currency module is working', () => { + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amt) => amt * 10) + }); + + it('should be used for actual conversions', () => { + expect(convertCurrency(123, 'c1', 'c2')).to.eql(1230); + sinon.assert.calledWith(getGlobal().convertCurrency, 123, 'c1', 'c2'); + }); + + it('should NOT be used when no conversion is necessary', () => { + expect(convertCurrency(123, 'cur', 'cur')).to.eql(123); + sinon.assert.notCalled(getGlobal().convertCurrency); + }) + }) + }); + + describe('Currency normalization', () => { + let mockConvert; + beforeEach(() => { + mockConvert = sinon.stub().callsFake((amt, from, to) => { + if (from === to) return amt; + return amt / from * to + }) + }); + + describe('currencyNormalizer', () => { + it('converts to toCurrency if set', () => { + const normalize = currencyNormalizer(10, true, mockConvert); + expect(normalize(1, 1)).to.eql(10); + expect(normalize(10, 100)).to.eql(1); + }); + + it('converts to first currency if toCurrency is not set', () => { + const normalize = currencyNormalizer(null, true, mockConvert); + expect(normalize(1, 1)).to.eql(1); + expect(normalize(1, 10)).to.eql(0.1); + }); + + [true, false].forEach(bestEffort => { + it(`passes bestEffort = ${bestEffort} to convert`, () => { + currencyNormalizer(null, bestEffort, mockConvert)(1, 1); + sinon.assert.calledWith(mockConvert, 1, 1, 1, bestEffort); + }) + }) + }); + + describe('currencyCompare', () => { + let compare + beforeEach(() => { + compare = currencyCompare((val) => [val.amount, val.cur], currencyNormalizer(null, false, mockConvert)) + }); + [ + [{amount: 1, cur: 1}, {amount: 1, cur: 10}, 1], + [{amount: 10, cur: 1}, {amount: 0.1, cur: 100}, 1], + [{amount: 1, cur: 1}, {amount: 10, cur: 10}, 0], + ].forEach(([a, b, expected]) => { + it(`should compare ${a.amount}/${a.cur} and ${b.amount}/${b.cur}`, () => { + expect(compare(a, b)).to.equal(expected); + expect(compare(b, a)).to.equal(-expected); + }); + }); + }) + }) +}) diff --git a/test/spec/libraries/sizeUtils_spec.js b/test/spec/libraries/sizeUtils_spec.js new file mode 100644 index 00000000000..1c954c6accf --- /dev/null +++ b/test/spec/libraries/sizeUtils_spec.js @@ -0,0 +1,30 @@ +import {getAdUnitSizes} from '../../../libraries/sizeUtils/sizeUtils.js'; +import {expect} from 'chai/index.js'; + +describe('getAdUnitSizes', function () { + it('returns an empty response when adUnits is undefined', function () { + let sizes = getAdUnitSizes(); + expect(sizes).to.be.undefined; + }); + + it('returns an empty array when invalid data is present in adUnit object', function () { + let sizes = getAdUnitSizes({sizes: 300}); + expect(sizes).to.deep.equal([]); + }); + + it('retuns an array of arrays when reading from adUnit.sizes', function () { + let sizes = getAdUnitSizes({sizes: [300, 250]}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({sizes: [[300, 250], [300, 600]]}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); + + it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { + let sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [300, 250]}}}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); +}); diff --git a/test/spec/libraries/urlUtils_spec.js b/test/spec/libraries/urlUtils_spec.js new file mode 100644 index 00000000000..9dd66b05407 --- /dev/null +++ b/test/spec/libraries/urlUtils_spec.js @@ -0,0 +1,24 @@ +import {tryAppendQueryString} from '../../../libraries/urlUtils/urlUtils.js'; +import assert from 'assert'; + +describe('tryAppendQueryString', function () { + it('should append query string to existing url', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = 'c'; + + var output = tryAppendQueryString(url, key, value); + + var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; + assert.equal(output, expectedResult); + }); + + it('should return existing url, if the value is empty', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = ''; + + var output = tryAppendQueryString(url, key, value); + assert.equal(output, url); + }); +}); diff --git a/test/spec/modules/33acrossAnalyticsAdapter_spec.js b/test/spec/modules/33acrossAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9e0d928cd97 --- /dev/null +++ b/test/spec/modules/33acrossAnalyticsAdapter_spec.js @@ -0,0 +1,1163 @@ +// @ts-nocheck +import analyticsAdapter from 'modules/33acrossAnalyticsAdapter.js'; +import { log } from 'modules/33acrossAnalyticsAdapter.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; +import * as events from 'src/events.js'; +import * as faker from 'faker'; +import CONSTANTS from 'src/constants.json'; +import { gdprDataHandler, gppDataHandler, uspDataHandler } from '../../../src/adapterManager'; +import { DEFAULT_ENDPOINT, POST_GAM_TIMEOUT, locals } from '../../../modules/33acrossAnalyticsAdapter'; +const { EVENTS, BID_STATUS } = CONSTANTS; + +describe('33acrossAnalyticsAdapter:', function () { + let sandbox; + let assert = getLocalAssert(); + + beforeEach(function () { + mockGpt.reset(); + + sandbox = sinon.createSandbox({ + useFakeTimers: { + now: new Date(2023, 3, 3, 0, 1, 33, 425), + }, + }); + + sandbox.stub(events, 'getEvents').returns([]); + + sandbox.spy(log, 'info'); + sandbox.spy(log, 'warn'); + sandbox.spy(log, 'error'); + + sandbox.stub(navigator, 'sendBeacon').callsFake(function (url, data) { + const json = JSON.parse(data); + assert.isValidAnalyticsReport(json); + + return true; + }); + }); + + afterEach(function () { + analyticsAdapter.disableAnalytics(); + mockGpt.enable(); + sandbox.restore(); + }); + + describe('enableAnalytics:', function () { + context('When pid is given', function () { + context('but endpoint is not', function () { + it('uses the default endpoint', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + }, + }); + + assert.equal(analyticsAdapter.getUrl(), DEFAULT_ENDPOINT); + }); + }); + + context('but the endpoint is invalid', function () { + it('logs an info message', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'foo' + }, + }); + + assert.calledWithExactly(log.info, 'Invalid endpoint provided for "options.endpoint". Using default endpoint.'); + }); + }); + }); + + context('When endpoint is given', function () { + context('but pid is not', function () { + it('logs an error message', function () { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: faker.internet.url() + }, + }); + + assert.calledWithExactly(log.error, 'No partnerId provided for "options.pid". No analytics will be sent.'); + }); + }); + }); + + context('When pid and endpoint are given', function () { + context('and an invalid timeout config value is given', function () { + it('logs an info message', function () { + [null, 'foo', -1].forEach(timeout => { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'http://test-endpoint', + timeout + }, + }); + analyticsAdapter.disableAnalytics(); + + assert.calledWithExactly(log.info, 'Invalid timeout provided for "options.timeout". Using default timeout of 10000ms.'); + log.info.resetHistory(); + }); + }); + }); + }); + }); + + // check that upcoming tests are derived from a valid report + describe('Report Mocks', function () { + it('the report should have the correct format', function () { + assert.isValidAnalyticsReport(createReportWithThreeBidWonEvents()); + }); + }); + + describe('Event Handling', function () { + beforeEach(function () { + this.defaultTimeout = 10000; + this.enableAnalytics = (options) => { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: 'http://test-endpoint', + pid: 'test-pid', + timeout: this.defaultTimeout, + ...options + }, + }); + window.googletag.cmd.forEach(cmd => cmd()); + } + }); + + context('when an auction is complete', function () { + context('and the AnalyticsReport is sent successfully to the given endpoint', function () { + it('calls "sendBeacon" with all won bids', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const [url, jsonString] = navigator.sendBeacon.firstCall.args; + const { auctions } = JSON.parse(jsonString); + + assert.lengthOf(mapToBids(auctions).filter(bid => bid.hasWon), 3); + }); + + it('calls "sendBeacon" with the correct report string', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + + it('logs an info message containing the report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())) + .returns(true); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.info, `Analytics report sent to ${endpoint}`, createReportWithThreeBidWonEvents()); + }); + + it('calls "sendBeacon" as soon as all values are available (before timeout)', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('and a valid US Privacy configuration is present', function () { + ['1YNY', '1---', '1NY-', '1Y--', '1--Y', '1N--', '1--N', '1NNN'].forEach(consent => { + it(`calls "sendBeacon" with a report containing the "${consent}" privacy string`, function () { + sandbox.stub(uspDataHandler, 'getConsentData').returns(consent); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + usPrivacy: consent + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('and a GDPR Privacy configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GDPR consent string', function () { + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ + consentString: 'foo', + gdprApplies: true + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gdpr: 1, + gdprConsent: 'foo' + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('when an auction is complete and a GPP configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GPP consent string', function () { + sandbox.stub(gppDataHandler, 'getConsentData').returns({ + gppString: 'gppString', + applicableSections: [7] + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gpp: 'gppString', + gppSid: [7] + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + + context('when an error occurs while sending the AnalyticsReport', function () { + it('logs an error', function () { + this.enableAnalytics(); + navigator.sendBeacon.returns(false); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.error, 'Analytics report exceeded User-Agent data limits and was not sent.', createReportWithThreeBidWonEvents()); + }); + }); + + context('when an auction report was already sent', function () { + context('and a new bid won event is returned after the report completes', function () { + it('finishes the auction without error', function () { + const incompleteAnalyticsReport = createReportWithThreeBidWonEvents(); + incompleteAnalyticsReport.auctions.forEach(auction => { + auction.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + delete bid.bidResponse; + bid.hasWon = 0; + bid.status = 'pending'; + }); + }); + }); + + this.enableAnalytics(); + const { prebid: [auction] } = getMockEvents(); + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + sandbox.clock.tick(this.defaultTimeout + 1000); + + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', incompleteAnalyticsReport); + }); + }); + + context('and another auction completes after that', function () { + it('sends the new report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + }); + + context('when two auctions overlap', function() { + it('sends a report for each auction', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + + context('when an AUCTION_END event is received before BID_WON events', function () { + it('sends a report with the bids that have won after all bids are won', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + const { prebid: [auction] } = getMockEvents(); + + performStandardAuction({ exclude: [EVENTS.BID_WON] }); + + assert.notCalled(navigator.sendBeacon); + for (let bidWon of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWon); + } + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('when a BID_WON event is received', function () { + context('and there is no record of that bid being requested', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const mockEvents = getMockEvents(); + const { prebid } = mockEvents; + const [auction] = prebid; + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + + const fakeBidWonEvent = Object.assign(auction.BID_WON[0], { + transactionId: 'foo' + }) + + events.emit(EVENTS.BID_WON, fakeBidWonEvent); + + const { auctionId, requestId } = fakeBidWonEvent; + assert.calledWithExactly(log.error, `Cannot find bid "${requestId}" in auction "${auctionId}".`); + }); + }); + }); + + context('when a BID_REJECTED event is received', function () { + it(`marks the rejected bid as "rejected"`, function () { + this.enableAnalytics(); + + const auction = getMockEvents().prebid[0]; + + // Start the auction + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + // Reject first bid + const bidToReject = auction.BID_REQUESTED[0].bids[0]; + events.emit(EVENTS.BID_REJECTED, auction.BID_REJECTED[0]); + + // Accept remaining bids + for (let i = 1; i < auction.BID_RESPONSE.length; ++i) { + events.emit(EVENTS.BID_RESPONSE, auction.BID_RESPONSE[i]); + }; + + // Complete the auction + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(this.defaultTimeout + 1); + + // Verify that we detected that the first bid was rejected + const expectedRejectedBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(expectedRejectedBid.status, 'rejected'); + }); + }); + + context('when a transaction does not reach its complete state', function () { + context('and a timeout config value has been given', function () { + context('and the timeout value has elapsed', function () { + it('logs a warning', function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(timeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + + it(`marks timed out bids as "timeout"`, function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + const request = getMockEvents().prebid[0].BID_REQUESTED[0]; + const bidToTimeout = request.bids[0]; + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + sandbox.clock.tick(1); + events.emit(EVENTS.BID_TIMEOUT, [{ + auctionId: request.auctionId, + bidId: bidToTimeout.bidId, + transactionId: bidToTimeout.transactionId, + }]); + sandbox.clock.tick(timeout + 1000); + + const timeoutBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(timeoutBid.status, 'timeout'); + }); + }); + }); + + context('and a timeout config value has not been given', function () { + context('and the default timeout has elapsed', function () { + it('logs an error', function () { + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + }) + }); + + context('and the `slotRenderEnded` event fired for all bids, but not all bids have won', function () { + context('and the GAM slot IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + context('and the slot element IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd'], useSlotElementIds: true}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + it('does NOT send a report if not all `slotRenderEnded` events have timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT - 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 0); + }); + }); + + context('and the `slotRenderEnded` event has fired for an unknown slot code', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + auction.AUCTION_INIT.adUnits[0].code = 'INVALID_AD_UNIT_CODE'; + + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.calledWithExactly(log.warn, + 'Could not find configured ad unit matching GAM render of slot:', + { slotName: `${adUnitCodes[0]} - ${adSlotElementIds[0]}` }); + }); + }); + + context('and the incomplete report has been sent successfully', function () { + it('sends a report string with any bids with rendered status set to hasWon: 1', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 1); + }); + + it('reports bids with only targetingSet status as hasWon: 0', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 0); + }); + + it('logs an info message', function () { + navigator.sendBeacon.returns(true); + + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWith(log.info, `Analytics report sent to ${endpoint}`); + }); + }); + }); + + context('when the transaction manager has open transactions', function () { + it('reports those transactions as pending', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.equal(manager.status().pending.length, auction.BID_REQUESTED[0].bids.length); + }); + + context('and a single bidWon event has triggered', function () { + it('completes the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + events.emit(EVENTS.BID_WON, auction.BID_WON[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + + context('and a single slotRenderEnded event has triggered', function () { + context('and the Google Ad Manager timeout has not elapsed', function () { + it('does NOT complete the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 0, + pending: auction.BID_REQUESTED[0].bids.length + }); + }); + }); + + context('and the Google Ad Manager timeout has elapsed', function () { + it('completes the transaction', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({timeout}); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + }); + }); + }); +}); + +const adUnitCodes = ['/19968336/header-bid-tag-0', '/19968336/header-bid-tag-1', '/17118521/header-bid-tag-2']; +const adSlotElementIds = ['ad-slot-div-0', 'ad-slot-div-1', 'ad-slot-div-2']; + +function performStandardAuction({ exclude = [], useSlotElementIds = false } = {}) { + const mockEvents = getMockEvents(); + const { prebid, gam } = mockEvents; + const [auction] = prebid; + + if (!exclude.includes(EVENTS.AUCTION_INIT)) { + if (useSlotElementIds) { + // With this option, identify the ad units by slot element IDs instead of GAM paths + auction.AUCTION_INIT.adUnits.forEach((adUnit, i) => { + adUnit.code = adSlotElementIds[i]; + }); + } + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + } + + if (!exclude.includes(EVENTS.BID_REQUESTED)) { + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + } + + if (!exclude.includes(EVENTS.BID_RESPONSE)) { + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + } + + if (!exclude.includes(EVENTS.AUCTION_END)) { + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + } + + if (!exclude.includes('slotRenderEnded')) { + for (let gEvent of gam.slotRenderEnded) { + mockGpt.emitEvent('slotRenderEnded', gEvent); + } + } + + if (!exclude.includes(EVENTS.BID_WON)) { + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + } +} + +function mapToBids(auctions) { + return auctions.flatMap( + auction => auction.adUnits.flatMap( + au => au.bids + ) + ); +} + +function getLocalAssert() { + function isValidAnalyticsReport(report) { + assert.containsAllKeys(report, ['analyticsVersion', 'pid', 'src', 'pbjsVersion', 'auctions']); + if ('usPrivacy' in report) { + assert.match(report.usPrivacy, /[0|1][Y|N|-]{3}/); + } + if ('gdpr' in report) { + assert.oneOf(report.gdpr, [0, 1]); + } + if (report.gdpr === 1) { + assert.isString(report.gdprConsent); + } + if ('gpp' in report) { + assert.isString(report.gpp); + assert.isArray(report.gppSid); + } + if ('coppa' in report) { + assert.oneOf(report.coppa, [0, 1]); + } + + assert.equal(report.analyticsVersion, '1.0.0'); + assert.isString(report.pid); + assert.isString(report.src); + assert.equal(report.pbjsVersion, '$prebid.version$'); + assert.isArray(report.auctions); + assert.isAbove(report.auctions.length, 0); + report.auctions.forEach(isValidAuction); + } + function isValidAuction(auction) { + assert.hasAllKeys(auction, ['adUnits', 'auctionId', 'userIds']); + assert.isArray(auction.adUnits); + assert.isString(auction.auctionId); + assert.isArray(auction.userIds); + auction.adUnits.forEach(isValidAdUnit); + } + function isValidAdUnit(adUnit) { + assert.hasAllKeys(adUnit, ['transactionId', 'adUnitCode', 'slotId', 'mediaTypes', 'sizes', 'bids']); + assert.isString(adUnit.transactionId); + assert.isString(adUnit.adUnitCode); + assert.isString(adUnit.slotId); + assert.isArray(adUnit.mediaTypes); + assert.isArray(adUnit.sizes); + assert.isArray(adUnit.bids); + adUnit.mediaTypes.forEach(isValidMediaType); + adUnit.sizes.forEach(isValidSizeString); + adUnit.bids.forEach(isValidBid); + } + function isValidBid(bid) { + assert.containsAllKeys(bid, ['bidder', 'bidId', 'source', 'status', 'hasWon']); + if ('bidResponse' in bid) { + isValidBidResponse(bid.bidResponse); + } + assert.isString(bid.bidder); + assert.isString(bid.bidId); + assert.isString(bid.source); + assert.oneOf(bid.status, ['pending', 'timeout', 'targetingSet', 'rendered', 'success', 'rejected', 'no-bid', 'error']); + assert.oneOf(bid.hasWon, [0, 1]); + } + function isValidBidResponse(bidResponse) { + assert.containsAllKeys(bidResponse, ['mediaType', 'size', 'cur', 'cpm', 'cpmFloor']); + if ('cpmOrig' in bidResponse) { + assert.isNumber(bidResponse.cpmOrig); + } + isValidMediaType(bidResponse.mediaType); + isValidSizeString(bidResponse.size); + assert.isString(bidResponse.cur); + assert.isNumber(bidResponse.cpm); + assert.isNumber(bidResponse.cpmFloor); + } + function isValidMediaType(mediaType) { + assert.oneOf(mediaType, ['banner', 'video', 'native']); + } + function isValidSizeString(size) { + assert.match(size, /[0-9]+x[0-9]+/); + } + + function calledOnceWithStringJsonEquivalent(sinonSpy, ...args) { + sinon.assert.calledOnce(sinonSpy); + args.forEach((arg, i) => { + const stubCallArgs = sinonSpy.firstCall.args[i] + + if (typeof arg === 'object') { + assert.deepEqual(JSON.parse(stubCallArgs), arg); + } else { + assert.strictEqual(stubCallArgs, arg); + } + }); + } + + sinon.assert.expose(assert, { prefix: '' }); + return { + ...assert, + calledOnceWithStringJsonEquivalent, + isValidAnalyticsReport, + isValidAuction, + isValidAdUnit, + isValidBid, + isValidBidResponse, + isValidMediaType, + isValidSizeString, + } +}; + +function createReportWithThreeBidWonEvents() { + return { + pid: 'test-pid', + src: 'pbjs', + analyticsVersion: '1.0.0', + pbjsVersion: '$prebid.version$', + auctions: [{ + adUnits: [{ + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + adUnitCode: adUnitCodes[0], + slotId: adUnitCodes[0], + mediaTypes: ['banner'], + sizes: ['300x250', '300x600'], + bids: [{ + bidder: 'bidder0', + bidId: '20661fc5fbb5d9b', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '300x250' + }, + hasWon: 1 + }] + }, { + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + adUnitCode: adUnitCodes[1], + slotId: adUnitCodes[1], + mediaTypes: ['banner'], + sizes: ['728x90', '970x250'], + bids: [{ + bidder: 'bidder0', + bidId: '21ad295f40dd7ab', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }, { + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + adUnitCode: adUnitCodes[2], + slotId: adUnitCodes[2], + mediaTypes: ['banner'], + sizes: ['300x250'], + bids: [{ + bidder: 'bidder0', + bidId: '22108ac7b778717', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }], + auctionId: 'auction-000', + userIds: ['33acrossId'] + }], + }; +} + +function getMockEvents() { + const auctionId = 'auction-000'; + const userId = { + '33acrossId': { + envelope: 'v1.0014', + }, + }; + + return { + gam: { + slotRenderEnded: [ + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[0], divId: adSlotElementIds[0] }), + isEmpty: true, + slotContentChanged: true, + size: null, + advertiserId: null, + campaignId: null, + creativeId: null, + creativeTemplateId: null, + labelIds: null, + lineItemId: null, + isBackfill: false, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[1], divId: adSlotElementIds[1] }), + isEmpty: false, + slotContentChanged: true, + size: [1, 1], + advertiserId: 12345, + campaignId: 400000001, + creativeId: 6789, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1011, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[2], divId: adSlotElementIds[2] }), + isEmpty: false, + slotContentChanged: true, + size: [728, 90], + advertiserId: 12346, + campaignId: 299999000, + creativeId: 6790, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1012, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + ], + }, + prebid: [{ + AUCTION_INIT: { + auctionId, + adUnits: [ + { + code: adUnitCodes[0], + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [300, 250], + [300, 600], + ], + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + ortb2Imp: { + ext: { + gpid: adUnitCodes[0], + }, + }, + }, + { + code: adUnitCodes[1], + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [970, 250], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [728, 90], + [970, 250], + ], + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + ortb2Imp: { + ext: { + gpid: adUnitCodes[1], + }, + }, + }, + { + code: adUnitCodes[2], + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: '33across', + userId, + }, + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [[300, 250]], + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + ortb2Imp: { + ext: { + gpid: adUnitCodes[2], + }, + }, + }, + ], + bidderRequests: [ + { + bids: [ + { userId }, + ], + } + ], + }, + BID_REQUESTED: [ + { + auctionId, + bids: [ + { + bidder: 'bidder0', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + bidId: '20661fc5fbb5d9b', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + bidId: '21ad295f40dd7ab', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + bidId: '22108ac7b778717', + src: 'client', + }, + ], + }], + BID_RESPONSE: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'targetingSet' + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'targetingSet', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'targetingSet', + }], + BID_WON: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'rendered', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + }], + BID_REJECTED: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 2 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + width: 300, + height: 250, + source: 'client', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + statusMessage: 'Bid available', + rejectionReason: 'Bid does not meet price floor', + }], + AUCTION_END: { + auctionId, + }, + }], + }; +} diff --git a/test/spec/modules/a1MediaBidAdapter_spec.js b/test/spec/modules/a1MediaBidAdapter_spec.js new file mode 100644 index 00000000000..e1db2b9ad8d --- /dev/null +++ b/test/spec/modules/a1MediaBidAdapter_spec.js @@ -0,0 +1,248 @@ +import { spec } from 'modules/a1MediaBidAdapter.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; +import 'modules/currency.js'; +import 'modules/priceFloors.js'; +import { replaceAuctionPrice } from '../../../src/utils'; + +const ortbBlockParams = { + battr: [ 13 ], + bcat: ['IAB1-1'] +}; +const getBidderRequest = (isMulti = false) => { + return { + bidderCode: 'a1media', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: '104e8d2392bd6f', + bids: [ + { + bidder: 'a1media', + params: {}, + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + mediaTypes: { + banner: { + sizes: [ + [ 320, 100 ], + ] + }, + ...(isMulti && { + video: { + mimes: ['video/mp4'] + }, + native: { + title: { + required: true, + }} + }) + }, + ...(isMulti && { + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + } + ] + } + }), + adUnitCode: 'test-div', + transactionId: 'cab00498-028b-4061-8f9d-a8d66c8cb91d', + bidId: '2e9f38ea93bb9e', + bidderRequestId: '104e8d2392bd6f', + } + ], + } +}; +const getConvertedBidReq = () => { + return { + cur: [ + 'JPY' + ], + imp: [ + { + banner: { + format: [ + { + h: 100, + w: 320 + }, + ], + topframe: 0 + }, + bidfloor: 0, + bidfloorcur: 'JPY', + id: '2e9f38ea93bb9e' + } + ], + test: 0, + } +}; + +const getBidderResponse = () => { + return { + body: { + id: 'bid-response', + cur: 'JPY', + seatbid: [ + { + bid: [{ + impid: '2e9f38ea93bb9e', + crid: 'creative-id', + cur: 'JPY', + price: 9, + }] + } + ] + } + } +} +const bannerAdm = '
'; +const videoAdm = 'testvast1'; +const nativeAdm = '{"ver":"1.2","link":{"url":"test_url"},"assets":[{"id":1,"required":1,"title":{"text":"native_title"}}]}'; +const macroAdm = '
'; +const macroNurl = 'https://d11.contentsfeed.com/dsp/win/example.com/SITE/a1/${AUCTION_PRICE}'; +const interpretedNurl = `
`; + +describe('a1MediaBidAdapter', function() { + describe('isValidRequest', function() { + const bid = { + bidder: 'a1media', + }; + + it('should return true always', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function() { + let bidderRequest, convertedRequest; + beforeEach(function() { + bidderRequest = getBidderRequest(); + convertedRequest = getConvertedBidReq(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + + expect(bidRequest.method).equal('POST'); + expect(bidRequest.url).equal('https://d11.contentsfeed.com/dsp/breq/a1'); + expect(bidRequest.data).deep.equal(convertedRequest); + }); + it('should set ortb blocking using params', function() { + bidderRequest.bids[0].params = ortbBlockParams; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.bcat = ortbBlockParams.bcat; + convertedRequest.imp[0].banner.battr = ortbBlockParams.battr; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + + it('should set bidfloor when getFloor is available', function() { + bidderRequest.bids[0].getFloor = () => ({ currency: 'USD', floor: 999 }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.imp[0].bidfloor).equal(999); + expect(bidRequest.data.imp[0].bidfloorcur).equal('USD'); + }); + + it('should set cur when currency config is configured', function() { + config.setConfig({ + currency: { + adServerCurrency: 'USD', + } + }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.cur[0]).equal('USD'); + }); + + it('should set bidfloor and currency using params when modules not available', function() { + bidderRequest.bids[0].params.currency = 'USD'; + bidderRequest.bids[0].params.bidfloor = 0.99; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.imp[0].bidfloor = 0.99; + convertedRequest.imp[0].bidfloorcur = 'USD'; + convertedRequest.cur[0] = 'USD'; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + }); + + describe('interpretResponse', function() { + describe('when request mediaType is single', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set cpm using price attribute', function() { + const bidResPrice = 9; + bidderResponse.body.seatbid[0].bid[0].price = bidResPrice; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].cpm).equal(bidResPrice); + }); + it('should set mediaType using request mediaTypes', function() { + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('when request mediaType is multi', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set mediaType to video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = videoAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(VIDEO); + }); + it('should set mediaType to native', function() { + bidderResponse.body.seatbid[0].bid[0].adm = nativeAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(NATIVE); + }); + it('should set mediaType to banner when adm is neither native or video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = bannerAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('resolve the AUCTION_PRICE macro', function() { + let bidRequest; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + }); + it('should return empty array when bid response has not contents', function() { + const emptyResponse = { body: '' }; + const interpretedRes = spec.interpretResponse(emptyResponse, bidRequest); + expect(interpretedRes.length).equal(0); + }); + it('should replace macro keyword if is exist', function() { + const bidderResponse = getBidderResponse(); + bidderResponse.body.seatbid[0].bid[0].adm = macroAdm; + bidderResponse.body.seatbid[0].bid[0].nurl = macroNurl; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + + const expectedResPrice = 9; + const expectedAd = replaceAuctionPrice(macroAdm, expectedResPrice) + replaceAuctionPrice(interpretedNurl, expectedResPrice); + + expect(interpretedRes[0].ad).equal(expectedAd); + }); + }); + }); +}) diff --git a/test/spec/modules/acuityadsBidAdapter_spec.js b/test/spec/modules/acuityadsBidAdapter_spec.js new file mode 100644 index 00000000000..31ef9dd6466 --- /dev/null +++ b/test/spec/modules/acuityadsBidAdapter_spec.js @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/acuityadsBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'acuityads' + +describe('AcuityAdsBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500, + ortb2: {} + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + describe('Returns data with gppConsent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/ad2ictionBidAdapter_spec.js b/test/spec/modules/ad2ictionBidAdapter_spec.js new file mode 100644 index 00000000000..99800c6dd01 --- /dev/null +++ b/test/spec/modules/ad2ictionBidAdapter_spec.js @@ -0,0 +1,223 @@ +import { expect } from 'chai'; +import { + spec, + API_ENDPOINT, + API_VERSION_NUMBER, +} from 'modules/ad2ictionBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('ad2ictionBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + }, + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '2a7a3b48778a1b', + bidderRequestId: '1e6509293abe6b', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params id is not valid (letters)', function () { + const mockBid = { + ...bid, + params: { id: 1234 }, + }; + + expect(spec.isBidRequestValid(mockBid)).to.equal(false); + }); + + it('should return false when params id is not exist', function () { + const mockBid = { + ...bid, + }; + delete mockBid.params.id; + + expect(spec.isBidRequestValid(mockBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const mockValidBidRequests = [ + { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '57ffc0667379e1', + bidderRequestId: '4ddea14478a651', + }, + ]; + + const mockBidderRequest = { + bidderCode: 'ad2iction', + bidderRequestId: '4ddea14478a651', + bids: [ + { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + adUnitCode: 'adunit-code', + transactionId: null, + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '57ffc0667379e1', + bidderRequestId: '4ddea14478a651', + }, + ], + timeout: 1200, + refererInfo: { + ref: 'https://example.com/referer.html', + }, + ortb2: { + source: {}, + site: { + ref: 'https://example.com/referer.html', + }, + device: { + w: 390, + h: 844, + language: 'zh', + }, + }, + start: 1702526505498, + }; + + it('should send bid request to API_ENDPOINT via POST', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.url).to.equal(API_ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send bid request with API version', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.data.v).to.equal(API_VERSION_NUMBER); + }); + + it('should send bid request with dada fields', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.data).to.include.all.keys('udid', '_'); + expect(request.data).to.have.property('refererInfo'); + expect(request.data).to.have.property('ortb2'); + }); + }); + + describe('interpretResponse', function () { + it('should return an empty array to indicate no valid bids', function () { + const mockServerResponse = {}; + + const bidResponses = spec.interpretResponse(mockServerResponse); + + expect(bidResponses).is.an('array').that.is.empty; + }); + + it('should return a valid bid response', function () { + const MOCK_AD_DOM = "
" + const mockServerResponse = { + body: [ + { + requestId: '23a3d87fb6bde9', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + { + requestId: '3ce3efc40c890b', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + ], + }; + + const exceptServerResponse = [ + { + requestId: '23a3d87fb6bde9', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + { + requestId: '3ce3efc40c890b', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + ] + + const bidResponses = spec.interpretResponse(mockServerResponse); + + expect(bidResponses).to.eql(exceptServerResponse); + }); + }); +}); diff --git a/test/spec/modules/adagioAnalyticsAdapter_spec.js b/test/spec/modules/adagioAnalyticsAdapter_spec.js index 581f3cb1b87..5ffd7b0b685 100644 --- a/test/spec/modules/adagioAnalyticsAdapter_spec.js +++ b/test/spec/modules/adagioAnalyticsAdapter_spec.js @@ -1,12 +1,14 @@ import adagioAnalyticsAdapter from 'modules/adagioAnalyticsAdapter.js'; import { expect } from 'chai'; import * as utils from 'src/utils.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); let constants = require('src/constants.json'); -describe('adagio analytics adapter', () => { +describe('adagio analytics adapter - adagio.js', () => { let sandbox; let adagioQueuePushSpy; @@ -174,3 +176,658 @@ describe('adagio analytics adapter', () => { }); }); }); + +const AUCTION_ID = '25c6d7f5-699a-4bfc-87c9-996f915341fa'; +const AUCTION_ID_ADAGIO = '6fc53663-bde5-427b-ab63-baa9ed296f47' +const AUCTION_ID_CACHE = 'b43d24a0-13d4-406d-8176-3181402bafc4'; +const AUCTION_ID_CACHE_ADAGIO = 'a9cae98f-efb5-477e-9259-27350044f8db'; + +const BID_ADAGIO = Object.assign({}, BID_ADAGIO, { + bidder: 'adagio', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.42, + currency: 'USD', + originalCpm: 1.42, + originalCurrency: 'USD', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + }, + pba: { + sid: '42', + e_pba_test: true + } +}); + +const BID_ANOTHER = Object.assign({}, BID_ANOTHER, { + bidder: 'another', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.71, + currency: 'EUR', + originalCpm: 1.62, + originalCurrency: 'GBP', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + } +}); + +const BID_CACHED = Object.assign({}, BID_ADAGIO, { + auctionId: AUCTION_ID_CACHE, + latestTargetedAuctionId: BID_ADAGIO.auctionId, +}); + +const PARAMS_ADG = { + organizationId: '1001', + site: 'test-com', + pageviewId: 'a68e6d70-213b-496c-be0a-c468ff387106', + environment: 'desktop', + pagetype: 'article', + placement: 'pave_top', + testName: 'test', + testVersion: 'version', +}; + +const AUCTION_INIT_ANOTHER = { + 'auctionId': AUCTION_ID, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'nobid', + 'params': { + 'publisherId': '1002' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'nobid', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_INIT_CACHE = { + 'auctionId': AUCTION_ID_CACHE, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_CACHE_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_END_ANOTHER = Object.assign({}, AUCTION_INIT_ANOTHER, { + bidsReceived: [BID_ANOTHER, BID_ADAGIO] +}); + +const AUCTION_END_ANOTHER_NOBID = Object.assign({}, AUCTION_INIT_ANOTHER, { + bidsReceived: [] +}); + +const MOCK = { + SET_TARGETING: { + [BID_ADAGIO.adUnitCode]: BID_ADAGIO.adserverTargeting, + [BID_ANOTHER.adUnitCode]: BID_ANOTHER.adserverTargeting + }, + AUCTION_INIT: { + another: AUCTION_INIT_ANOTHER, + bidcached: AUCTION_INIT_CACHE + }, + BID_RESPONSE: { + adagio: BID_ADAGIO, + another: BID_ANOTHER + }, + AUCTION_END: { + another: AUCTION_END_ANOTHER, + another_nobid: AUCTION_END_ANOTHER_NOBID + }, + BID_WON: { + adagio: Object.assign({}, BID_ADAGIO, { + 'status': 'rendered' + }), + another: Object.assign({}, BID_ANOTHER, { + 'status': 'rendered' + }), + bidcached: Object.assign({}, BID_CACHED, { + 'status': 'rendered' + }), + }, + AD_RENDER_SUCCEEDED: { + another: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_ANOTHER + }, + bidcached: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + }, + AD_RENDER_FAILED: { + bidcached: { + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + } +}; + +describe('adagio analytics adapter', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.registerAnalyticsAdapter({ + code: 'adagio', + adapter: adagioAnalyticsAdapter + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('track', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'adagio' + }); + }); + + afterEach(() => { + adagioAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', () => { + getGlobal().convertCurrency = (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + }; + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.another); + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); + + expect(server.requests.length).to.equal(3, 'requests count'); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,nobid'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('1,1,0'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('another'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_cpm).to.equal('1.71'); + expect(search.cur).to.equal('EUR'); + expect(search.cur_rate).to.equal('1.2'); + expect(search.og_cpm).to.equal('1.62'); + expect(search.og_cur).to.equal('GBP'); + expect(search.og_cur_rate).to.equal('1.6'); + } + }); + + it('builds and sends auction data with a cached bid win', () => { + getGlobal().convertCurrency = (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + }; + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.bidcached); + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another_nobid); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.bidcached); + events.emit(constants.EVENTS.AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED.bidcached); + + expect(server.requests.length).to.equal(5, 'requests count'); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another'); + expect(search.adg_mts).to.equal('ban'); + expect(search.t_n).to.equal('test'); + expect(search.t_v).to.equal('version'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,nobid'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('0,0,0'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('adagio'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_cpm).to.equal('1.42'); + expect(search.cur).to.equal('USD'); + expect(search.cur_rate).to.equal('1'); + expect(search.og_cpm).to.equal('1.42'); + expect(search.og_cur).to.equal('USD'); + expect(search.og_cur_rate).to.equal('1'); + expect(search.rndr).to.not.exist; + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[4].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('4'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.rndr).to.equal('0'); + } + }); + }); +}); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 1f734a6a7fc..744f3c69e83 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -861,11 +861,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(3); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({f: 1, mt: 'banner', s: '300x250'}); - expect(requests[0].data.adUnits[0].floors[1]).to.deep.equal({f: 1, mt: 'banner', s: '300x600'}); - expect(requests[0].data.adUnits[0].floors[2]).to.deep.equal({f: 1, mt: 'video', s: '600x480'}); - expect(requests[0].data.adUnits[0].mediaTypes.banner.sizes.length).to.equal(2); expect(requests[0].data.adUnits[0].mediaTypes.banner.bannerSizes[0]).to.deep.equal({size: [300, 250], floor: 1}); expect(requests[0].data.adUnits[0].mediaTypes.banner.bannerSizes[1]).to.deep.equal({size: [300, 600], floor: 1}); @@ -890,10 +885,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(2); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({f: 1, mt: 'video'}); - expect(requests[0].data.adUnits[0].floors[1]).to.deep.equal({f: 1, mt: 'native'}); - expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.equal(1); expect(requests[0].data.adUnits[0].mediaTypes.native.floor).to.equal(1); }); @@ -913,8 +904,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(1); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({mt: 'video'}); expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.be.undefined; }); }); @@ -958,6 +947,34 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.usIfr).to.equal(false); }); }); + + describe('with GPID', function () { + const gpid = '/12345/my-gpt-tag-0'; + + it('should add preferred gpid to the request', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { + gpid: gpid + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); + }); + + it('should add backup gpid to the request', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { + data: { pbadslot: gpid } + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); + }); + }); }); describe('interpretResponse()', function() { diff --git a/test/spec/modules/adfusionBidAdapter_spec.js b/test/spec/modules/adfusionBidAdapter_spec.js new file mode 100644 index 00000000000..638831c33f3 --- /dev/null +++ b/test/spec/modules/adfusionBidAdapter_spec.js @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adfusionBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('adfusionBidAdapter', function () { + const adapter = newBidder(spec); + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params.accountID is missing', function () { + let localbid = Object.assign({}, bid); + delete localbid.params.accountId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [ + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }, + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + }, + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2', + }, + ]; + bidderRequest = { refererInfo: {} }; + }); + + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], bidderRequest); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.an('array'); + expect(request[0].data).to.be.an('object'); + expect(request[0].method).to.equal('POST'); + expect(request[0].url).to.not.equal(''); + expect(request[0].url).to.not.equal(undefined); + expect(request[0].url).to.not.equal(null); + }); + }); +}); diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 90200a801c5..ade34478c20 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -15,11 +15,13 @@ describe('Adkernel adapter', function () { auctionId: 'auc-001', mediaTypes: { banner: { - sizes: [[300, 250], [300, 200]] + sizes: [[300, 250], [300, 200]], + pos: 1 } }, ortb2Imp: { - battr: [6, 7, 9] + battr: [6, 7, 9], + pos: 2 } }, bid2_zone2 = { bidder: 'adkernel', @@ -103,7 +105,11 @@ describe('Adkernel adapter', function () { video: { context: 'instream', playerSize: [[640, 480]], - api: [1, 2] + api: [1, 2], + placement: 1, + plcmt: 1, + skip: 1, + pos: 1 } }, adUnitCode: 'ad-unit-1' @@ -244,6 +250,31 @@ describe('Adkernel adapter', function () { }], bidid: 'pTuOlf5KHUo', cur: 'EUR' + }, + multiformat_response = { + id: '47ce4badcf7482', + seatbid: [{ + bid: [{ + id: 'sZSYq5zYMxo_0', + impid: 'Bid_01b__mf', + crid: '100_003', + price: 0.00145, + adid: '158801', + adm: '', + nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl', + cid: '16855' + }, { + id: 'sZSYq5zYMxo_1', + impid: 'Bid_01v__mf', + crid: '100_003', + price: 0.25, + adid: '158801', + nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_1&f=nurl', + cid: '16855' + }] + }], + bidid: 'pTuOlf5KHUo', + cur: 'USD' }; var sandbox; @@ -346,6 +377,11 @@ describe('Adkernel adapter', function () { expect(bidRequest.imp[0].banner.battr).to.be.eql([6, 7, 9]); }); + it('should respect mediatypes attributes over FPD', function() { + expect(bidRequest.imp[0].banner).to.have.property('pos'); + expect(bidRequest.imp[0].banner.pos).to.be.eql(1); + }); + it('shouldn\'t contain gdpr nor ccpa information for default request', function () { let [_, bidRequests] = buildRequest([bid1_zone1]); expect(bidRequests[0]).to.not.have.property('regs'); @@ -438,24 +474,40 @@ describe('Adkernel adapter', function () { }); it('should have openrtb video impression parameters', function() { - expect(bidRequests[0].imp[0].video).to.have.property('api'); - expect(bidRequests[0].imp[0].video.api).to.be.eql([1, 2]); + let video = bidRequests[0].imp[0].video; + expect(video).to.have.property('api'); + expect(video.api).to.be.eql([1, 2]); + expect(video.placement).to.be.eql(1); + expect(video.plcmt).to.be.eql(1); + expect(video.skip).to.be.eql(1); + expect(video.pos).to.be.eql(1); }); }); describe('multiformat request building', function () { - let _, bidRequests; + let pbRequests, bidRequests; before(function () { - [_, bidRequests] = buildRequest([bid_multiformat]); + [pbRequests, bidRequests] = buildRequest([bid_multiformat]); }); it('should contain single request', function () { expect(bidRequests).to.have.length(1); - expect(bidRequests[0].imp).to.have.length(1); }); - it('should contain banner-only impression', function () { - expect(bidRequests[0].imp).to.have.length(1); + it('should contain both impression', function () { + expect(bidRequests[0].imp).to.have.length(2); expect(bidRequests[0].imp[0]).to.have.property('banner'); - expect(bidRequests[0].imp[0]).to.not.have.property('video'); + expect(bidRequests[0].imp[1]).to.have.property('video'); + // check that splitted imps do not share same impid + expect(bidRequests[0].imp[0].id).to.be.not.eql('Bid_01'); + expect(bidRequests[0].imp[1].id).to.be.not.eql('Bid_01'); + expect(bidRequests[0].imp[1].id).to.be.not.eql(bidRequests[0].imp[0].id); + }); + it('x', function() { + let bids = spec.interpretResponse({body: multiformat_response}, pbRequests[0]); + expect(bids).to.have.length(2); + expect(bids[0].requestId).to.be.eql('Bid_01'); + expect(bids[0].mediaType).to.be.eql('banner'); + expect(bids[1].requestId).to.be.eql('Bid_01'); + expect(bids[1].mediaType).to.be.eql('video'); }); }); diff --git a/test/spec/modules/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index 8c9969e4d46..b4d84634962 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -1,12 +1,553 @@ -import {expect} from 'chai'; -import {spec, storage} from 'modules/admaticBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; +import { expect } from 'chai'; +import { spec } from 'modules/admaticBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; const ENDPOINT = 'https://layer.serve.admatic.com.tr/pb'; describe('admaticBidAdapter', () => { const adapter = newBidder(spec); + let validRequest = [ { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, + 'bidder': 'admatic', + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [728, 90]] + }, + 'native': { + }, + 'video': { + } + }, + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'pixad.com.tr', + 'sid': 'px-pub-3000856707', + 'hp': 1 + } + ] + }, + 'at': 1, + 'tmax': 1000, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ] + } + }, + 'ortb': { + 'badv': [], + 'bcat': [], + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' + } + }, + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost', + 'publisherId': 12321312 + } + }, + 'imp': [ + { + 'size': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 728, + 'h': 90 + } + ], + 'mediatype': {}, + 'type': 'banner', + 'id': '2205da7a81846b', + 'floors': { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + } + }, + { + 'size': [ + { + 'w': 338, + 'h': 280 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 338, + 280 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2 + }, + 'floors': { + 'video': { + '338x280': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'floors': { + 'native': { + '*': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '16e0c8982318f91' + } + ], + 'ext': { + 'cur': 'USD', + 'bidder': 'admatic' + } + } ]; + let bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, + 'bidder': 'admatic', + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [728, 90]] + }, + 'native': { + }, + 'video': { + 'playerSize': [ + 336, + 280 + ] + } + }, + 'userId': { + 'id5id': { + 'uid': '0', + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + }, + 'pubcid': '5a49273f-a424-454b-b478-169c3551aa72' + }, + 'userIdAsEids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ], + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'pixad.com.tr', + 'sid': 'px-pub-3000856707', + 'hp': 1 + } + ] + }, + 'at': 1, + 'tmax': 1000, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ] + } + }, + 'ortb': { + 'source': {}, + 'site': { + 'domain': 'localhost:8888', + 'publisher': { + 'domain': 'localhost:8888' + }, + 'page': 'http://localhost:8888/', + 'name': 'http://localhost:8888' + }, + 'badv': [], + 'bcat': [], + 'device': { + 'w': 896, + 'h': 979, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '119' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '119' + ] + }, + { + 'brand': 'Not?A_Brand', + 'version': [ + '24' + ] + } + ], + 'mobile': 0 + } + } + }, + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost', + 'publisherId': 12321312 + } + }, + 'imp': [ + { + 'size': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 728, + 'h': 90 + } + ], + 'id': '2205da7a81846b', + 'mediatype': {}, + 'type': 'banner', + 'floors': { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + } + }, + { + 'size': [ + { + 'w': 338, + 'h': 280 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 338, + 280 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2 + }, + 'floors': { + 'video': { + '338x280': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'floors': { + 'native': { + '*': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '16e0c8982318f91' + } + ], + 'ext': { + 'cur': 'USD', + 'bidder': 'admatic' + } + }; describe('inherited functions', () => { it('exists and is a function', () => { @@ -16,6 +557,10 @@ describe('admaticBidAdapter', () => { describe('isBidRequestValid', function() { let bid = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, @@ -47,255 +592,147 @@ describe('admaticBidAdapter', () => { describe('buildRequests', function () { it('sends bid request to ENDPOINT via POST', function () { - let validRequest = [ { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2Imp': { 'ext': { 'instl': 1 } }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } - }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else { - return {} - } - }, - 'user': { - 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' - }, - 'blacklist': [], - 'site': { - 'page': 'http://localhost:8888/admatic.html', - 'ref': 'http://localhost:8888', - 'publisher': { - 'name': 'localhost', - 'publisherId': 12321312 + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should not populate GDPR if for non-EEA users', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } - }, - 'imp': [ - { - 'size': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 728, - 'h': 90 - } - ], - 'mediatype': {}, - 'type': 'banner', - 'id': '2205da7a81846b', - 'floors': { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - } - } - }, - { - 'size': [ - { - 'w': 338, - 'h': 280 - } - ], - 'type': 'video', - 'mediatype': { - 'context': 'instream', - 'mimes': [ - 'video/mp4' - ], - 'maxduration': 240, - 'api': [ - 1, - 2 - ], - 'playerSize': [ - [ - 338, - 280 - ] - ], - 'protocols': [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - 'skip': 1, - 'playbackmethod': [ - 2 - ], - 'linearity': 1, - 'placement': 2 - }, - 'id': '45e86fc7ce7fc93' + }) + ); + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.regs.ext.consent).to.equal('BOJ8RZsOJ8RZsABAB8AAAAAZ-A'); + }); + + it('should populate GDPR and empty consent string if available for EEA users without consent string but with consent', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true } - ], - 'ext': { - 'cur': 'USD', - 'bidder': 'admatic' + }) + ); + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.regs.ext.consent).to.equal(''); + }); + + it('should properly build a request when coppa flag is true', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + coppa: true + }) + ); + expect(request.data.regs.ext.coppa).to.not.be.undefined; + expect(request.data.regs.ext.coppa).to.equal(1); + }); + + it('should properly build a request with gpp consent field', function () { + let bidRequest = Object.assign([], validRequest); + const ortb2 = { + regs: { + gpp: 'gpp_consent_string', + gpp_sid: [0, 1, 2] } - } ]; - let bidderRequest = { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2Imp': { 'ext': { 'instl': 1 } }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } - }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else { - return {} - } - }, - 'user': { - 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' - }, - 'blacklist': [], - 'site': { - 'page': 'http://localhost:8888/admatic.html', - 'ref': 'http://localhost:8888', - 'publisher': { - 'name': 'localhost', - 'publisherId': 12321312 - } - }, - 'imp': [ - { - 'size': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 728, - 'h': 90 - } - ], - 'id': '2205da7a81846b', - 'mediatype': {}, - 'type': 'banner', - 'floors': { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - } + }; + const request = spec.buildRequests(bidRequest, { ...bidderRequest, ortb2 }); + expect(request.data.regs.ext.gpp).to.equal('gpp_consent_string'); + expect(request.data.regs.ext.gpp_sid).to.deep.equal([0, 1, 2]); + }); + + it('should properly build a request with ccpa consent field', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + uspConsent: '1---' + }) + ); + expect(request.data.regs.ext.uspIab).to.not.be.null; + expect(request.data.regs.ext.uspIab).to.equal('1---'); + }); + + it('should properly forward eids', function () { + const bidRequests = [ + { + bidder: 'admatic', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] } }, - { - 'size': [ - { - 'w': 338, - 'h': 280 - } - ], - 'type': 'video', - 'mediatype': { - 'context': 'instream', - 'mimes': [ - 'video/mp4' - ], - 'maxduration': 240, - 'api': [ - 1, - 2 - ], - 'playerSize': [ - [ - 338, - 280 - ] - ], - 'protocols': [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - 'skip': 1, - 'playbackmethod': [ - 2 - ], - 'linearity': 1, - 'placement': 2 - }, - 'id': '45e86fc7ce7fc93' - } - ], - 'ext': { - 'cur': 'USD', - 'bidder': 'admatic' + userIdAsEids: [ + { + source: 'admatic.com.tr', + uids: [{ + id: 'abc', + atype: 1 + }] + } + ], + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.user.ext.eids).to.deep.equal([ + { + source: 'admatic.com.tr', + uids: [{ + id: 'abc', + atype: 1 + }] } - }; - const request = spec.buildRequests(validRequest, bidderRequest); - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('POST'); + ]); }); it('should properly build a banner request with floors', function () { - let bidRequests = [ + const request = spec.buildRequests(validRequest, bidderRequest); + request.data.imp[0].floors = { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }; + }); + + it('should properly build a video request with several player sizes with floors', function () { + const bidRequests = [ { 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, + 'adUnitCode': 'bid-123', + 'transactionId': 'transaction-123', 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] + 'video': { + 'playerSize': [[300, 250], [728, 90]] } }, 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + if (inputParams.mediaType === VIDEO && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', floor: 1.0 }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', floor: 2.0 @@ -306,33 +743,50 @@ describe('admaticBidAdapter', () => { } }, ]; - let bidderRequest = { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2Imp': { 'ext': { 'instl': 1 } }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [728, 90]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'creativeId': 'er2ee', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } + const bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', } }; const request = spec.buildRequests(bidRequests, bidderRequest); - request.data.imp[0].floors = { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } + }); + + it('should properly build a native request with floors', function () { + const bidRequests = [ + { + 'bidder': 'admatic', + 'adUnitCode': 'bid-123', + 'transactionId': 'transaction-123', + 'mediaTypes': { + 'native': { + } + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + getFloor: inputParams => { + if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + } + }, + ]; + const bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', } }; + const request = spec.buildRequests(bidRequests, bidderRequest); }); }); @@ -348,6 +802,7 @@ describe('admaticBidAdapter', () => { 'price': 0.01, 'type': 'banner', 'bidder': 'admatic', + 'mime_type': 'iframe', 'adomain': ['admatic.com.tr'], 'party_tag': '
', 'iurl': 'https://www.admatic.com.tr' @@ -359,6 +814,7 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', + 'mime_type': 'iframe', 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': '', @@ -371,10 +827,24 @@ describe('admaticBidAdapter', () => { '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', + '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"]}}', + 'iurl': 'https://www.admatic.com.tr' } ], 'queryId': 'cdnbh24rlv0hhkpfpln0', @@ -393,6 +863,7 @@ describe('admaticBidAdapter', () => { ad: '
', creativeId: '374', meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -410,6 +881,7 @@ describe('admaticBidAdapter', () => { vastXml: '', creativeId: '3741', meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -427,6 +899,41 @@ describe('admaticBidAdapter', () => { 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', + mediaType: 'native', + netRevenue: true, + native: { + 'clickUrl': 'https://www.admatic.com.tr', + 'impressionTrackers': ['https://www.admatic.com.tr'], + 'title': 'title', + 'body': 'body', + 'sponsoredBy': 'sponsored', + 'cta': 'cta', + 'image': { + 'url': 'https://www.admatic.com.tr', + 'width': 1200, + 'height': 628 + }, + 'icon': { + 'url': 'https://www.admatic.com.tr', + 'width': 640, + 'height': 480 + } + }, + creativeId: '3742', + meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 8cf433460b7..85538efc957 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -4,11 +4,12 @@ import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config.js'; const BIDDER_CODE = 'admixer'; -const BIDDER_CODE_ADX = 'admixeradx'; +const WL_BIDDER_CODE = 'admixerwl' const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; const ENDPOINT_URL_CUSTOM = 'https://custom.admixer.net/prebid.aspx'; -const ENDPOINT_URL_ADX = 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; +const CLIENT_ID = 5124; +const ENDPOINT_ID = 81264; describe('AdmixerAdapter', function () { const adapter = newBidder(spec); @@ -36,9 +37,28 @@ describe('AdmixerAdapter', function () { auctionId: '1d1a030790a475', }; + let wlBid = { + bidder: WL_BIDDER_CODE, + params: { + clientId: CLIENT_ID, + endpointId: ENDPOINT_ID, + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + it('should return true when required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + it('should return true when params required by WL found', function () { + expect(spec.isBidRequestValid(wlBid)).to.equal(true); + }); it('should return false when required params are not passed', function () { let bid = Object.assign({}, bid); @@ -48,6 +68,14 @@ describe('AdmixerAdapter', function () { }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when params required by WL are not passed', function () { + let wlBid = Object.assign({}, wlBid); + delete wlBid.params; + wlBid.params = { + clientId: 0, + }; + expect(spec.isBidRequestValid(wlBid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -105,7 +133,10 @@ describe('AdmixerAdapter', function () { validRequest: [ { bidder: bidder, - params: { + params: bidder === 'admixerwl' ? { + clientId: CLIENT_ID, + endpointId: ENDPOINT_ID + } : { zone: ZONE_ID, }, adUnitCode: 'adunit-code', @@ -168,6 +199,12 @@ describe('AdmixerAdapter', function () { expect(request.url).to.equal('https://inv-nets.admixer.net/adxprebid.1.2.aspx'); expect(request.method).to.equal('POST'); }); + it('build request for admixerwl', function () { + const requestParams = requestParamsFor('admixerwl'); + const request = spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest); + expect(request.url).to.equal(`https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx?client=${CLIENT_ID}`); + expect(request.method).to.equal('POST'); + }); }); describe('checkFloorGetting', function () { diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 4ddbaaa2e2a..e109ca1829c 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -10,9 +10,10 @@ import {getGlobal} from '../../../src/prebidGlobal'; describe('adnuntiusBidAdapter', function() { const URL = 'https://ads.adnuntius.delivery/i?tzo='; const EURO_URL = 'https://europe.delivery.adnuntius.com/i?tzo='; - const GVLID = 855; const usi = utils.generateUUID() - const meta = [{key: 'usi', value: usi}] + + const meta = [{key: 'valueless'}, {value: 'keyless'}, {key: 'voidAuIds'}, {key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp()}, {exp: misc.getUnixTimestamp(1)}]}, {key: 'valid', value: 'also-valid', exp: misc.getUnixTimestamp(1)}, {key: 'expired', value: 'fwefew', exp: misc.getUnixTimestamp()}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}, {key: 'usi', value: usi, exp: misc.getUnixTimestamp(100)}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}] + let storage; before(() => { getGlobal().bidderSettings = { @@ -20,8 +21,11 @@ describe('adnuntiusBidAdapter', function() { storageAllowed: true } }; - const storage = getStorageManager({bidderCode: 'adnuntius'}) - storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + storage = getStorageManager({bidderCode: 'adnuntius'}); + }); + + beforeEach(() => { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)); }); after(() => { @@ -38,7 +42,7 @@ describe('adnuntiusBidAdapter', function() { const ENDPOINT_URL_VIDEO = `${ENDPOINT_URL_BASE}&userId=${usi}&tt=vast4`; const ENDPOINT_URL_NOCOOKIE = `${ENDPOINT_URL_BASE}&userId=${usi}&noCookies=true`; const ENDPOINT_URL_SEGMENTS = `${ENDPOINT_URL_BASE}&segments=segment1,segment2,segment3&userId=${usi}`; - const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=json&consentString=consentString&userId=${usi}`; + const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=json&consentString=consentString&gdpr=1&userId=${usi}`; const adapter = newBidder(spec); const bidderRequests = [ @@ -47,6 +51,7 @@ describe('adnuntiusBidAdapter', function() { bidder: 'adnuntius', params: { auId: '000000000008b6bc', + targetId: '123', network: 'adnuntius', maxDeals: 1 }, @@ -99,7 +104,10 @@ describe('adnuntiusBidAdapter', function() { const videoBidRequest = { bid: videoBidderRequest, - bidder: 'adnuntius' + bidder: 'adnuntius', + params: { + bidType: 'justsomestuff-error-handling' + } } const deals = [ @@ -456,7 +464,78 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url'); expect(request[0].url).to.equal(ENDPOINT_URL); expect(request[0]).to.have.property('data'); - expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"adn-000000000008b6bc","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","maxDeals":0,"dimensions":[[1640,1480],[1600,1400]]}],"metaData":{"usi":"' + usi + '"}}'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}],"metaData":{"valid":"also-valid"}}'); + }); + + it('Test requests with no local storage', function() { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{}])); + const request = spec.buildRequests(bidderRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL_BASE); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}]}'); + + localStorage.removeItem('adn.metaData'); + const request2 = spec.buildRequests(bidderRequests, {}); + expect(request2.length).to.equal(1); + expect(request2[0]).to.have.property('url'); + expect(request2[0].url).to.equal(ENDPOINT_URL_BASE); + }); + + it('Test request changes for voided au ids', function() { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp(1)}, {auId: '0000000000000023', exp: misc.getUnixTimestamp(1)}]}])); + const bRequests = bidderRequests.concat([{ + bidId: 'adn-11118b6bc', + bidder: 'adnuntius', + params: { + auId: '11118b6bc', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + }]); + bRequests.push({ + bidId: 'adn-23', + bidder: 'adnuntius', + params: { + auId: '23', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + }); + bRequests.push({ + bidId: 'adn-13', + bidder: 'adnuntius', + params: { + auId: '13', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[164, 140], [10, 1400]], + } + }, + }); + const request = spec.buildRequests(bRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL_BASE); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]},{"auId":"13","targetId":"adn-13","dimensions":[[164,140],[10,1400]]}]}'); }); it('Test Video requests', function() { @@ -474,7 +553,7 @@ describe('adnuntiusBidAdapter', function() { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}] + segment: [{id: 'segment1'}, {id: 'segment2'}, {invalidSegment: 'invalid'}, {id: 123}, {id: ['3332']}] }, { name: 'other', @@ -657,7 +736,7 @@ describe('adnuntiusBidAdapter', function() { expect(bidderRequests[0].params.maxDeals).to.equal(1); expect(data.adUnits[0].maxDeals).to.equal(bidderRequests[0].params.maxDeals); expect(bidderRequests[1].params).to.not.have.property('maxBids'); - expect(data.adUnits[1].maxDeals).to.equal(0); + expect(data.adUnits[1].maxDeals).to.equal(undefined); }); it('Should allow a maximum of 5 deals.', function() { config.setBidderConfig({ @@ -703,7 +782,7 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('data'); const data = JSON.parse(request[0].data); expect(data.adUnits.length).to.equal(1); - expect(data.adUnits[0].maxDeals).to.equal(0); + expect(data.adUnits[0].maxDeals).to.equal(undefined); }); it('Should set max deals using bidder config.', function() { config.setBidderConfig({ @@ -749,12 +828,20 @@ describe('adnuntiusBidAdapter', function() { describe('interpretResponse', function() { it('should return valid response when passed valid server response', function() { - const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); + config.setBidderConfig({ + bidders: ['adnuntius'], + config: { + bidType: 'netBid', + maxDeals: 1 + } + }); + + const interpretedResponse = config.runWithBidder('adnuntius', () => spec.interpretResponse(serverResponse, singleBidRequest)); expect(interpretedResponse).to.have.lengthOf(2); const deal = serverResponse.body.adUnits[0].deals[0]; expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[0].cpm).to.equal(deal.bid.amount * 1000); + expect(interpretedResponse[0].cpm).to.equal(deal.netBid.amount * 1000); expect(interpretedResponse[0].width).to.equal(Number(deal.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(deal.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(deal.creativeId); @@ -770,7 +857,7 @@ describe('adnuntiusBidAdapter', function() { const ad = serverResponse.body.adUnits[0].ads[0]; expect(interpretedResponse[1].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[1].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[1].cpm).to.equal(ad.netBid.amount * 1000); expect(interpretedResponse[1].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[1].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[1].creativeId).to.equal(ad.creativeId); @@ -783,6 +870,33 @@ describe('adnuntiusBidAdapter', function() { expect(interpretedResponse[1].ttl).to.equal(360); expect(interpretedResponse[1].dealId).to.equal('not-in-deal-array-here'); expect(interpretedResponse[1].dealCount).to.equal(0); + + const results = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')); + const usiEntry = results.find(entry => entry.key === 'usi'); + expect(usiEntry.key).to.equal('usi'); + expect(usiEntry.value).to.equal('from-api-server dude'); + expect(usiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); + + const voidAuIdsEntry = results.find(entry => entry.key === 'voidAuIds'); + expect(voidAuIdsEntry.key).to.equal('voidAuIds'); + expect(voidAuIdsEntry.exp).to.equal(undefined); + expect(voidAuIdsEntry.value[0].auId).to.equal('00000000000abcde'); + expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + expect(voidAuIdsEntry.value[1].auId).to.equal('00000000000fffff'); + expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + + const validEntry = results.find(entry => entry.key === 'valid'); + expect(validEntry.key).to.equal('valid'); + expect(validEntry.value).to.equal('also-valid'); + expect(validEntry.exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(validEntry.exp).to.be.lessThan(misc.getUnixTimestamp(2)); + + const randomApiEntry = results.find(entry => entry.key === 'randomApiKey'); + expect(randomApiEntry.key).to.equal('randomApiKey'); + expect(randomApiEntry.value).to.equal('randomApiValue'); + expect(randomApiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); }); it('should not process valid response when passed alt bidder that is an adndeal', function() { @@ -795,6 +909,7 @@ describe('adnuntiusBidAdapter', function() { ] }; serverResponse.body.adUnits[0].deals = []; + delete serverResponse.body.metaData.voidAuIds; // test response with no voidAuIds const interpretedResponse = spec.interpretResponse(serverResponse, altBidder); expect(interpretedResponse).to.have.lengthOf(0); @@ -808,6 +923,9 @@ describe('adnuntiusBidAdapter', function() { { bidder: 'adn-alt', bidId: 'adn-0000000000000551', + params: { + bidType: 'netBid' + } } ] }; @@ -818,7 +936,7 @@ describe('adnuntiusBidAdapter', function() { const ad = serverResponse.body.adUnits[0].ads[0]; expect(interpretedResponse[0].bidderCode).to.equal('adn-alt'); - expect(interpretedResponse[0].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[0].cpm).to.equal(ad.netBid.amount * 1000); expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); diff --git a/test/spec/modules/adpod_spec.js b/test/spec/modules/adpod_spec.js index a6164f919ef..14e530c1a9b 100644 --- a/test/spec/modules/adpod_spec.js +++ b/test/spec/modules/adpod_spec.js @@ -47,7 +47,6 @@ describe('adpod.js', function () { addBidToAuctionStub = sinon.stub(auction, 'addBidToAuction').callsFake(function (auctionInstance, bid) { auctionBids.push(bid); }); - doCallbacksIfTimedoutStub = sinon.stub(auction, 'doCallbacksIfTimedout'); clock = sinon.useFakeTimers(); config.setConfig({ cache: { @@ -61,7 +60,6 @@ describe('adpod.js', function () { logWarnStub.restore(); logInfoStub.restore(); addBidToAuctionStub.restore(); - doCallbacksIfTimedoutStub.restore(); clock.restore(); config.resetConfig(); auctionBids = []; @@ -633,7 +631,6 @@ describe('adpod.js', function () { callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, videoMT); callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, videoMT); - expect(doCallbacksIfTimedoutStub.calledTwice).to.equal(true); expect(logWarnStub.calledOnce).to.equal(true); expect(auctionBids.length).to.equal(0); }); diff --git a/test/spec/modules/adstirBidAdapter_spec.js b/test/spec/modules/adstirBidAdapter_spec.js new file mode 100644 index 00000000000..290a6822f69 --- /dev/null +++ b/test/spec/modules/adstirBidAdapter_spec.js @@ -0,0 +1,412 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/adstirBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; + +describe('AdstirAdapter', function () { + describe('isBidRequestValid', function () { + it('should return true if appId is non-empty string and adSpaceNo is integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false if appId is non-empty string, but adSpaceNo is not integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 'a', + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is null', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: null, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is undefined', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX' + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is empty string', function () { + const bid = { + params: { + appId: '', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is not string', function () { + const bid = { + params: { + appId: 123, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is null', function () { + const bid = { + params: { + appId: null, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is undefined', function () { + const bid = { + params: { + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if params is empty', function () { + const bid = { + params: {} + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const validBidRequests = [ + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid1111', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 1, + }, + transactionId: 'transactionid-1111', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + } + }, + sizes: [ + [300, 250], + [336, 280], + ], + }, + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid2222', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 2, + }, + transactionId: 'transactionid-2222', + mediaTypes: { + banner: { + sizes: [ + [320, 50], + [320, 100], + ], + } + }, + sizes: [ + [320, 50], + [320, 100], + ], + }, + ]; + + const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: 'https://ad-stir.com/contact', + topmostLocation: 'https://ad-stir.com/contact', + reachedTop: true, + ref: 'https://test.example/q=adstir', + isAmp: false, + numIframes: 0, + stack: [ + 'https://ad-stir.com/contact', + ], + }, + }; + + it('one entry in validBidRequests corresponds to one server request object.', function () { + const requests = spec.buildRequests(validBidRequests, bidderRequest); + expect(requests.length).to.equal(validBidRequests.length); + requests.forEach(function (r, i) { + expect(r.method).to.equal('POST'); + expect(r.url).to.equal('https://ad.ad-stir.com/prebid'); + const d = JSON.parse(r.data); + expect(d.auctionId).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); + + const v = validBidRequests[i]; + expect(d.appId).to.equal(v.params.appId); + expect(d.adSpaceNo).to.equal(v.params.adSpaceNo); + expect(d.bidId).to.equal(v.bidId); + expect(d.transactionId).to.equal(v.transactionId); + expect(d.mediaTypes).to.deep.equal(v.mediaTypes); + expect(d.sizes).to.deep.equal(v.sizes); + expect(d.ref.page).to.equal(bidderRequest.refererInfo.page); + expect(d.ref.tloc).to.equal(bidderRequest.refererInfo.topmostLocation); + expect(d.ref.referrer).to.equal(bidderRequest.refererInfo.ref); + expect(d.sua).to.equal(null); + expect(d.gdpr).to.equal(false); + expect(d.usp).to.equal(false); + expect(d.schain).to.equal(null); + expect(d.eids).to.deep.equal([]); + }); + }); + + it('ref.page, ref.tloc and ref.referrer correspond to refererInfo', function () { + const [ request ] = spec.buildRequests([validBidRequests[0]], { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: null, + topmostLocation: 'https://adserver.example/iframe1.html', + reachedTop: false, + ref: null, + isAmp: false, + numIframes: 2, + stack: [ + null, + 'https://adserver.example/iframe1.html', + 'https://adserver.example/iframe2.html' + ], + }, + }); + + const { ref } = JSON.parse(request.data); + expect(ref.page).to.equal(null); + expect(ref.tloc).to.equal('https://adserver.example/iframe1.html'); + expect(ref.referrer).to.equal(null); + }); + + it('when config.pageUrl is not set, ref.topurl equals to refererInfo.reachedTop', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(reachedTop); + }); + }); + + describe('when config.pageUrl is set, ref.topurl is always false', function () { + before(function () { + config.setConfig({ pageUrl: 'https://ad-stir.com/register' }); + }); + after(function () { + config.resetConfig(); + }); + + it('ref.topurl should be false', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(false); + }); + }); + }); + + it('gdprConsent.gdprApplies is sent', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (gdprApplies) { + bidderRequestClone.gdprConsent = { gdprApplies }; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.gdpr).to.equal(gdprApplies); + }); + }); + + it('includes in the request parameters whether CCPA applies', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + const cases = [ + { uspConsent: '1---', expected: false }, + { uspConsent: '1YYY', expected: true }, + { uspConsent: '1YNN', expected: true }, + { uspConsent: '1NYN', expected: true }, + { uspConsent: '1-Y-', expected: true }, + ]; + cases.forEach(function ({ uspConsent, expected }) { + bidderRequestClone.uspConsent = uspConsent; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.usp).to.equal(expected); + }); + }); + + it('should add schain if available', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher, Inc.', + 'domain': 'publisher.example' + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.example' + } + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,bid-request-1,publisher%2C%20Inc.,publisher.example!exchange2.example,abcd,1,bid-request-2,intermediary,intermediary.example'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add schain even if some nodes params are blank', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + }, + { + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + }, + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,,,!,,,,,!exchange2.example,abcd,1,,,'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add UA client hints to payload if available', function () { + const sua = { + browsers: [ + { + brand: 'Not?A_Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + version: [ + '108', + '0', + '5359', + '40' + ] + }, + { + brand: 'Google Chrome', + version: [ + '108', + '0', + '5359', + '40' + ] + } + ], + platform: { + brand: 'Android', + version: [ + '11' + ] + }, + mobile: 1, + architecture: '', + bitness: '64', + model: 'Pixel 5', + source: 2 + } + + const validBidRequestsClone = utils.deepClone(validBidRequests); + validBidRequestsClone[0] = utils.mergeDeep(validBidRequestsClone[0], { + ortb2: { + device: { sua }, + } + }); + + const requests = spec.buildRequests(validBidRequestsClone, bidderRequest); + requests.forEach(function (r) { + const d = JSON.parse(r.data); + expect(d.sua).to.deep.equal(sua); + }); + }); + }); + + describe('interpretResponse', function () { + it('return empty array when no content', function () { + const bids = spec.interpretResponse({ body: '' }); + expect(bids).to.deep.equal([]); + }); + it('return empty array when seatbid empty', function () { + const bids = spec.interpretResponse({ body: { seatbid: [] } }); + expect(bids).to.deep.equal([]); + }); + it('return valid bids when serverResponse is valid', function () { + const serverResponse = { + 'body': { + 'seatbid': [ + { + 'bid': { + 'ad': '
test response
', + 'cpm': 5250, + 'creativeId': '5_1234ABCD', + 'currency': 'JPY', + 'height': 250, + 'meta': { + 'advertiserDomains': [ + 'adv.example' + ], + 'mediaType': 'banner', + 'networkId': 5 + }, + 'netRevenue': true, + 'requestId': '22a9457aed98a4', + 'transactionId': 'f18c078e-4d2a-4ecb-a886-2a0c52187213', + 'ttl': 60, + 'width': 300, + } + } + ] + }, + 'headers': {} + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids[0]).to.deep.equal(serverResponse.body.seatbid[0].bid); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index e40828e6852..0acbaa06f5b 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,18 +11,13 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/' ]; const aliasEP = { - 'appaloosa': 'https://ghb.hb.appaloosa.media/v2/auction/', - 'appaloosa_publisherSuffix': 'https://ghb.hb.appaloosa.media/v2/auction/', - 'onefiftytwomedia': 'https://ghb.ads.152media.com/v2/auction/', - 'navelix': 'https://ghb.hb.navelix.com/v2/auction/', - 'bidsxchange': 'https://ghb.hbd.bidsxchange.com/v2/auction/', + 'janet_publisherSuffix': 'https://ghb.bidder.jmgads.com/v2/auction/', 'streamkey': 'https://ghb.hb.streamkey.net/v2/auction/', 'janet': 'https://ghb.bidder.jmgads.com/v2/auction/', - 'pgam': 'https://ghb.pgamssp.com/v2/auction/', 'ocm': 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', - 'vidcrunchllc': 'https://ghb.platform.vidcrunch.com/v2/auction/', '9dotsmedia': 'https://ghb.platform.audiodots.com/v2/auction/', 'copper6': 'https://ghb.app.copper6.com/v2/auction/', + 'indicue': 'https://ghb.console.indicue.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 65c7584b428..e07e3a6e5d4 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,835 +1,19 @@ // jshint esversion: 6, es3: false, node: true -import {assert} from 'chai'; -import {spec} from 'modules/adxcgBidAdapter.js'; -import {config} from 'src/config.js'; -import {createEidsArray} from 'modules/userId/eids.js'; +import { assert } from 'chai'; +import { spec } from 'modules/adxcgBidAdapter.js'; +import { config } from 'src/config.js'; +import { createEidsArray } from 'modules/userId/eids.js'; +/* eslint dot-notation:0, quote-props:0 */ +import { expect } from 'chai'; + +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; +import { deepClone } from '../../../src/utils'; + const utils = require('src/utils'); describe('Adxcg adapter', function () { let bids = []; - describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'adxcg', - 'params': { - 'adzoneid': '19910113' - } - }; - - it('should return true when required params found', function () { - assert(spec.isBidRequestValid(bid)); - - bid.params = { - adzoneid: 4332, - }; - assert(spec.isBidRequestValid(bid)); - }); - - it('should return false when required params are missing', function () { - bid.params = {}; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - mname: 'some-placement' - }; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - inv: 1234 - }; - assert.isFalse(spec.isBidRequestValid(bid)); - }); - }); - - describe('buildRequests', function () { - beforeEach(function () { - config.resetConfig(); - }); - it('should send request with correct structure', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: { - adzoneid: '19910113' - } - }]; - let request = spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}); - - assert.equal(request.method, 'POST'); - assert.equal(request.url, 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); - assert.deepEqual(request.options, {contentType: 'application/json'}); - assert.ok(request.data); - }); - - describe('user privacy', function () { - it('should send GDPR Consent data to exchange if gdprApplies', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, bidderRequest.gdprConsent.consentString); - assert.equal(request.regs.ext.gdpr, bidderRequest.gdprConsent.gdprApplies); - assert.equal(typeof request.regs.ext.gdpr, 'number'); - }); - - it('should send gdpr as number', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(typeof request.regs.ext.gdpr, 'number'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should send CCPA Consent data to exchange', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = {uspConsent: '1YA-', refererInfo: {referer: 'page'}}; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - - bidderRequest = { - uspConsent: '1YA-', - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should not send GDPR Consent data to adxcg if gdprApplies is undefined', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let bidderRequest = { - gdprConsent: {gdprApplies: false, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 0); - - bidderRequest = {gdprConsent: {consentString: 'consentDataString'}, refererInfo: {referer: 'page'}}; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - it('should send default GDPR Consent data to exchange', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - }); - - it('should add test and is_debug to request, if test is set in parameters', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {test: 1} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.ok(request.is_debug); - assert.equal(request.test, 1); - }); - - it('should have default request structure', function () { - let keys = 'site,geo,device,source,ext,imp'.split(','); - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - let data = Object.keys(request); - - assert.deepEqual(keys, data); - }); - - it('should set request keys correct values', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'}, - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, { - refererInfo: {referer: 'page'}, - ortb2: {source: {tid: 'tid'}} - }).data); - - assert.equal(request.source.tid, 'tid'); - assert.equal(request.source.fd, 1); - }); - - it('should send info about device', function () { - config.setConfig({ - device: {w: 100, h: 100} - }); - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}).data); - - assert.equal(request.device.ua, navigator.userAgent); - assert.equal(request.device.w, 100); - assert.equal(request.device.h, 100); - }); - - it('should send app info', function () { - config.setConfig({ - app: {id: 'appid'}, - }); - const ortb2 = {app: {name: 'appname'}} - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}, ortb2}).data); - - assert.equal(request.app.id, 'appid'); - assert.equal(request.app.name, 'appname'); - assert.equal(request.site, undefined); - }); - - it('should send info about the site', function () { - config.setConfig({ - site: { - id: '123123', - publisher: { - domain: 'publisher.domain.com' - } - }, - }); - const ortb2 = { - site: { - publisher: { - id: 4441, - name: 'publisher\'s name' - } - } - }; - - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let refererInfo = {page: 'page', domain: 'localhost'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo, ortb2}).data); - - assert.deepEqual(request.site, { - domain: 'localhost', - id: '123123', - page: refererInfo.page, - publisher: { - domain: 'publisher.domain.com', - id: 4441, - name: 'publisher\'s name' - } - }); - }); - - it('should pass extended ids', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }) - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.user.ext.eids, [ - {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} - ]); - }); - - it('should send currency if defined', function () { - config.setConfig({currency: {adServerCurrency: 'EUR'}}); - let validBidRequests = [{params: {}}]; - let refererInfo = {referer: 'page'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo}).data); - - assert.deepEqual(request.cur, ['EUR']); - }); - - it('should pass supply chain object', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - schain: { - validation: 'strict', - config: { - ver: '1.0' - } - } - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.source.ext.schain, { - validation: 'strict', - config: { - ver: '1.0' - } - }); - }); - - describe('bids', function () { - it('should add more than one bid to the request', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }, { - bidId: 'bidId2', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.imp.length, 2); - }); - it('should add incrementing values of id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId2', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId3', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].id, i + 1); - } - }); - - it('should add adzoneid', function () { - let validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}, - {bidId: 'bidId2', params: {adzoneid: 1001}, mediaTypes: {video: {}}}, - {bidId: 'bidId3', params: {adzoneid: 1002}, mediaTypes: {video: {}}}]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].tagid, validBidRequests[i].params.adzoneid); - } - }); - - describe('price floors', function () { - it('should not add if floors module not configured', function () { - const validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, undefined); - }); - - it('should not add if floor price not defined', function () { - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'USD'); - }); - - it('should request floor price in adserver currency', function () { - config.setConfig({currency: {adServerCurrency: 'DKK'}}); - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'DKK'); - }); - - it('should add correct floor values', function () { - const expectedFloors = [1, 1.3, 0.5]; - const validBidRequests = expectedFloors.map(getBidWithFloor); - let imps = getRequestImps(validBidRequests); - - expectedFloors.forEach((floor, index) => { - assert.equal(imps[index].bidfloor, floor); - assert.equal(imps[index].bidfloorcur, 'USD'); - }); - }); - - function getBidWithFloor(floor) { - return { - params: {adzoneid: 1}, - mediaTypes: {video: {}}, - getFloor: ({currency}) => { - return { - currency: currency, - floor - }; - } - }; - } - }); - - describe('multiple media types', function () { - it('should use all configured media types for bidding', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - video: {} - } - }, { - bidId: 'bidId1', - params: {adzoneid: 1000}, - mediaTypes: { - video: {}, - native: {} - } - }, { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140} - }, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - native: {}, - video: {} - } - }]; - let [first, second, third] = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - assert.ok(first.banner); - assert.ok(first.video); - assert.equal(first.native, undefined); - - assert.ok(second.video); - assert.equal(second.banner, undefined); - assert.equal(second.native, undefined); - - assert.ok(third.native); - assert.ok(third.video); - assert.ok(third.banner); - }); - }); - - describe('banner', function () { - it('should convert sizes to openrtb format', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - } - } - }]; - let {banner} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(banner, { - format: [{w: 100, h: 100}, {w: 200, h: 300}] - }); - }); - }); - - describe('video', function () { - it('should pass video mediatype config', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - } - } - }]; - let {video} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(video, { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - }); - }); - }); - - describe('native', function () { - describe('assets', function () { - it('should set correct asset id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }]; - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].id, 0); - assert.equal(assets[1].id, 3); - assert.equal(assets[2].id, 4); - }); - it('should add required key if it is necessary', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140}, - sponsoredBy: {required: true, len: 140} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].required, 1); - assert.ok(!assets[1].required); - assert.ok(!assets[2].required); - assert.equal(assets[3].required, 1); - }); - - it('should map img and data assets', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: true, sizes: [150, 50]}, - icon: {required: false, sizes: [50, 50]}, - body: {required: false, len: 140}, - sponsoredBy: {required: true}, - cta: {required: false}, - clickUrl: {required: false} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].title); - assert.equal(assets[0].title.len, 140); - assert.deepEqual(assets[1].img, {type: 3, w: 150, h: 50}); - assert.deepEqual(assets[2].img, {type: 1, w: 50, h: 50}); - assert.deepEqual(assets[3].data, {type: 2, len: 140}); - assert.deepEqual(assets[4].data, {type: 1}); - assert.deepEqual(assets[5].data, {type: 12}); - assert.ok(!assets[6]); - }); - - describe('icon/image sizing', function () { - it('should flatten sizes and utilise first pair', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - sizes: [[200, 300], [100, 200]] - }, - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.w, 200); - assert.equal(assets[0].img.h, 300); - }); - }); - - it('should utilise aspect_ratios', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - min_width: 100, - ratio_height: 3, - ratio_width: 1 - }] - }, - icon: { - aspect_ratios: [{ - min_width: 10, - ratio_height: 5, - ratio_width: 2 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 100); - assert.equal(assets[0].img.hmin, 300); - - assert.ok(assets[1].img); - assert.equal(assets[1].img.wmin, 10); - assert.equal(assets[1].img.hmin, 25); - }); - - it('should not throw error if aspect_ratios config is not defined', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [] - }, - icon: { - aspect_ratios: [] - } - } - }]; - - assert.doesNotThrow(() => spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}})); - }); - }); - - it('should expect any dimensions if min_width not passed', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - ratio_height: 3, - ratio_width: 1 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 0); - assert.equal(assets[0].img.hmin, 0); - assert.ok(!assets[1]); - }); - }); - }); - - function getRequestImps(validBidRequests) { - return JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - } - }); - - describe('interpretResponse', function () { - it('should return if no body in response', function () { - let serverResponse = {}; - let bidRequest = {}; - - assert.ok(!spec.interpretResponse(serverResponse, bidRequest)); - }); - it('should return more than one bids', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{ - impid: '1', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, title: {text: 'Asset title text'}}]} - }] - }, { - bid: [{ - impid: '2', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, data: {value: 'Asset title text'}}]} - }] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }, - { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(spec.interpretResponse(serverResponse, bidRequest).length, 2); - }); - - it('should set correct values to bid', function () { - let nativeExample1 = { - assets: [], - link: {url: 'link'}, - imptrackers: ['imptrackers url1', 'imptrackers url2'] - } - - let serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{ - bid: [ - { - impid: '1', - price: 93.1231, - crid: '12312312', - adm: JSON.stringify(nativeExample1), - dealid: 'deal-id', - adomain: ['demo.com'], - ext: { - crType: 'native', - advertiser_id: 'adv1', - advertiser_name: 'advname', - agency_name: 'agname', - mediaType: 'native' - } - } - ] - }], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - const bids = spec.interpretResponse(serverResponse, bidRequest); - const bid = serverResponse.body.seatbid[0].bid[0]; - assert.deepEqual(bids[0].requestId, bidRequest.bids[0].bidId); - assert.deepEqual(bids[0].cpm, bid.price); - assert.deepEqual(bids[0].creativeId, bid.crid); - assert.deepEqual(bids[0].ttl, 300); - assert.deepEqual(bids[0].netRevenue, false); - assert.deepEqual(bids[0].currency, serverResponse.body.cur); - assert.deepEqual(bids[0].mediaType, 'native'); - assert.deepEqual(bids[0].meta.mediaType, 'native'); - assert.deepEqual(bids[0].meta.advertiserDomains, ['demo.com']); - - assert.deepEqual(bids[0].meta.advertiserName, 'advname'); - assert.deepEqual(bids[0].meta.agencyName, 'agname'); - - assert.deepEqual(bids[0].dealId, 'deal-id'); - }); - - it('should return empty when there is no bids in response', function () { - const serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{bid: []}], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [{bidId: 'bidId1'}] - }; - const result = spec.interpretResponse(serverResponse, bidRequest)[0]; - assert.ok(!result); - }); - - describe('banner', function () { - it('should set ad content on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'banner'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].ad, ''); - assert.equal(bids[0].mediaType, 'banner'); - assert.equal(bids[0].meta.mediaType, 'banner'); - }); - }); - - describe('video', function () { - it('should set vastXml on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'video'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].vastXml, ''); - assert.equal(bids[0].mediaType, 'video'); - assert.equal(bids[0].meta.mediaType, 'video'); - }); - }); - }); - describe('getUserSyncs', function () { const usersyncUrl = 'https://usersync-url.com'; beforeEach(() => { @@ -846,55 +30,55 @@ describe('Adxcg adapter', function () { }) it('should return user sync if pixel enabled with adxcg config', function () { - const ret = spec.getUserSyncs({pixelEnabled: true}) - expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + const ret = spec.getUserSyncs({ pixelEnabled: true }) + expect(ret).to.deep.equal([{ type: 'image', url: usersyncUrl }]) }) it('should not return user sync if pixel disabled', function () { - const ret = spec.getUserSyncs({pixelEnabled: false}) + const ret = spec.getUserSyncs({ pixelEnabled: false }) expect(ret).to.be.an('array').that.is.empty }) it('should not return user sync if url is not set', function () { config.resetConfig() - const ret = spec.getUserSyncs({pixelEnabled: true}) + const ret = spec.getUserSyncs({ pixelEnabled: true }) expect(ret).to.be.an('array').that.is.empty }) - it('should pass GDPR consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + it('should pass GDPR consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` }]); }); - it('should pass US consent', function() { + it('should pass US consent', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` }]); }); - it('should pass GDPR and US consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + it('should pass GDPR and US consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); }); - describe('onBidWon', function() { - beforeEach(function() { + describe('onBidWon', function () { + beforeEach(function () { sinon.stub(utils, 'triggerPixel'); }); - afterEach(function() { + afterEach(function () { utils.triggerPixel.restore(); }); - it('Should trigger pixel if bid nurl', function() { + it('Should trigger pixel if bid nurl', function () { const bid = { nurl: 'http://example.com/win/${AUCTION_PRICE}', cpm: 2.1, @@ -904,4 +88,510 @@ describe('Adxcg adapter', function () { expect(utils.triggerPixel.callCount).to.equal(1) }) }) + + it('should return just to have at least 1 karma test ok', function () { + assert(true); + }); +}); + +describe('adxcg v8 oRtbConverter Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '300x250', + adzoneid: '77' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + params: { + cp: 'p10000', + ct: 't20000', + cf: '728x90', + adzoneid: '77' + } + }]; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 0, + data: { + type: 1 + } + }] + }; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaTypes: { + native: { + sendTargetingKeys: false, + ortb: nativeOrtbRequest + } + }, + nativeOrtbRequest, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slotVideo', + bidId: 'bid12345', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const additionalParamsConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345, + extra_key3: { + key1: 'val1', + key2: 23456, + }, + extra_key4: [1, 2, 3] + } + }]; + + const schainParamsSlotConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + bcat: ['IAB-1', 'IAB-20'], + battr: [1, 2, 3], + bidfloor: 1.5, + badv: ['cocacola.com', 'lays.com'] + }, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } + }; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + // expect(ortbRequest.site.publisher.id).to.equal('p10000'); + expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + // expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }, { 'w': 160, 'h': 600 }]); + // slot 2 + // expect(ortbRequest.imp[1].tagid).to.equal('t20000'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }]); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + mtype: 1, + w: 300, + h: 250, + exp: 20, + adomain: ['advertiser.com'] + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creative_id).to.equal('Creative#123'); + expect(bid.creativeId).to.equal('Creative#123'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('EUR'); + expect(bid.ttl).to.equal(20); + expect(bid.meta).to.not.be.null; + expect(bid.meta.advertiserDomains).to.eql(['advertiser.com']); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs, bidderRequest); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + if (FEATURES.NATIVE) { + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('77'); + expect(ortbRequest.imp[0].banner).to.be.undefined; + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(3); + // image asset + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.be.undefined; + expect(nativeRequest.assets[0].img).to.not.equal(null); + expect(nativeRequest.assets[0].img.w).to.equal(150); + expect(nativeRequest.assets[0].img.h).to.equal(50); + expect(nativeRequest.assets[0].img.type).to.equal(3); + // title asset + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[1].required).to.equal(1); + expect(nativeRequest.assets[1].title).to.not.equal(null); + expect(nativeRequest.assets[1].title.len).to.equal(80); + // data asset + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[2].title).to.be.undefined; + expect(nativeRequest.assets[2].data).to.not.equal(null); + expect(nativeRequest.assets[2].data.type).to.equal(1); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const nativeResponse = { + assets: [ + { id: 1, img: { type: 3, url: 'https://images.cdn.brand.com/123' } }, + { id: 2, title: { text: 'Ad Title' } }, + { id: 3, data: { type: 1, value: 'Sponsored By: Brand' } } + ], + link: { url: 'https://brand.clickme.com/' }, + imptrackers: ['https://imp1.trackme.com/', 'https://imp1.contextweb.com/'] + + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: JSON.stringify(nativeResponse), + mtype: 4 + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + expect(bid['native']).to.not.be.null; + expect(bid['native'].ortb).to.not.be.null; + const nativeBid = bid['native'].ortb; + expect(nativeBid.assets).to.have.lengthOf(3); + expect(nativeBid.assets[0].id).to.equal(1); + expect(nativeBid.assets[0].img).to.not.be.null; + expect(nativeBid.assets[0].img.type).to.equal(3); + expect(nativeBid.assets[0].img.url).to.equal('https://images.cdn.brand.com/123'); + expect(nativeBid.assets[1].id).to.equal(2); + expect(nativeBid.assets[1].title).to.not.be.null; + expect(nativeBid.assets[1].title.text).to.equal('Ad Title'); + expect(nativeBid.assets[2].id).to.equal(3); + expect(nativeBid.assets[2].data).to.not.be.null; + expect(nativeBid.assets[2].data.type).to.equal(1); + expect(nativeBid.assets[2].data.value).to.equal('Sponsored By: Brand'); + expect(nativeBid.link).to.not.be.null; + expect(nativeBid.link.url).to.equal('https://brand.clickme.com/'); + expect(nativeBid.imptrackers).to.have.lengthOf(2); + expect(nativeBid.imptrackers[0]).to.equal('https://imp1.trackme.com/'); + expect(nativeBid.imptrackers[1]).to.equal('https://imp1.contextweb.com/'); + }); + } + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('adxcg'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('mediaopti'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + if (FEATURES.VIDEO) { + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video).to.not.be.null; + expect(ortbRequest.imp[0].native).to.be.undefined; + expect(ortbRequest.imp[0].banner).to.be.undefined; + expect(ortbRequest.imp[0].video.w).to.equal(400); + expect(ortbRequest.imp[0].video.h).to.equal(300); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.maxduration).to.equal(10); + expect(ortbRequest.imp[0].video.startdelay).to.equal(0); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); + expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); + }); + } + + it('Verify extra parameters', function () { + let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key1).to.equal('extra_val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key2).to.equal(12345); + expect(ortbRequest.imp[0].ext.prebid.extra_key3).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key1).to.equal('val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key2).to.equal(23456); + expect(ortbRequest.imp[0].ext.prebid.extra_key4).to.eql([1, 2, 3]); + expect(Object.keys(ortbRequest.imp[0].ext.prebid)).to.eql(['adzoneid', 'extra_key1', 'extra_key2', 'extra_key3', 'extra_key4']); + // attempting with a configuration with no unknown params. + request = spec.buildRequests(videoSlotConfig, bidderRequest); + ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + // expect(ortbRequest.imp[0].ext).to.be.undefined; + }); + + it('Verify user level first party data', function () { + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + }, + ortb2: { + user: { + yob: 1985, + gender: 'm', + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.user).to.not.equal(null); + }); + + it('Verify site level first party data', function () { + const bidderRequest = { + ortb2: { + site: { + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + domain: 'pub.com' + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site).to.deep.equal({ + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + // id: 'p10000', + domain: 'pub.com' + } + }); + }); + + it('Verify impression/slot level first party data', function () { + const bidderRequests = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + } + } + }]; + let request = spec.buildRequests(bidderRequests, bidderRequest); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext).to.deep.equal({ + prebid: { + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + }); + }); + + it('Verify bid request timeouts', function () { + const mkRequest = (bidderRequest) => spec.buildRequests(slotConfigs, bidderRequest).data; + // assert default is used when no bidderRequest.timeout value is available + expect(mkRequest(bidderRequest).tmax).to.equal(500) + + // assert bidderRequest value is used when available + expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) + }); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index de77c741364..ffd6729397a 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adyoulikeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; describe('Adyoulike Adapter', function () { const canonicalUrl = 'https://canonical.url/?t=%26'; @@ -887,4 +888,115 @@ describe('Adyoulike Adapter', function () { expect(spec.gvlid).to.equal(259) }) }); + + describe('getUserSyncs', function () { + const syncurl_iframe = 'https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4'; + + const emptySync = []; + + describe('with iframe enabled', function() { + const userSyncConfig = { iframeEnabled: true }; + + it('should not add parameters if not provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should add GDPR parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo?'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo%3F` + }]); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: false, consentString: 'bar'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=bar` + }]); + }); + + it('should add CCPA parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, 'foo?')).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&us_privacy=foo%3F` + }]); + }); + + describe('COPPA', function() { + let sandbox; + + this.beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + this.afterEach(function() { + sandbox.restore(); + }); + + it('should add coppa parameters if provided', function() { + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': true + }; + return config[key]; + }); + + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&coppa=1` + }]); + }); + }); + + describe('GPP', function() { + it('should not apply if not gppConsent.gppString', function() { + const gppConsent = { gppString: '', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if not gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: undefined }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if empty gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: [] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should apply if all above are available', function() { + const gppConsent = { gppString: 'foo?', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo%3F&gpp_sid=123` + }]); + }); + + it('should support multiple sections', function() { + const gppConsent = { gppString: 'foo', applicableSections: [123, 456] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo&gpp_sid=123%2C456` + }]); + }); + }); + }); + + describe('with iframe disabled', function() { + const userSyncConfig = { iframeEnabled: false }; + + it('should return empty list of syncs', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal(emptySync); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo'}, 'bar')).to.deep.equal(emptySync); + }); + }); + }); }); diff --git a/test/spec/modules/agmaAnalyticsAdapter_spec.js b/test/spec/modules/agmaAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..ba71624e3b3 --- /dev/null +++ b/test/spec/modules/agmaAnalyticsAdapter_spec.js @@ -0,0 +1,388 @@ +import adapterManager from '../../../src/adapterManager.js'; +import agmaAnalyticsAdapter, { + getTiming, + getOrtb2Data, + getPayload, +} from '../../../modules/agmaAnalyticsAdapter.js'; +import { gdprDataHandler } from '../../../src/adapterManager.js'; +import { expect } from 'chai'; +import * as events from '../../../src/events.js'; +import constants from '../../../src/constants.json'; +import { generateUUID } from '../../../src/utils.js'; +import { server } from '../../mocks/xhr.js'; +import { config } from 'src/config.js'; + +const INGEST_URL = 'https://pbc.agma-analytics.de/v1'; +const extendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'extended', + 'gdprApplies', + 'gdprConsentString', + 'language', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'referrer', + 'screenHeight', + 'screenWidth', + 'scriptVersion', + 'timestamp', + 'timezoneOffset', + 'timing', + 'triggerEvent', + 'userIdsAsEids', +]; +const nonExtendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'gdprApplies', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'scriptVersion', + 'timing', + 'triggerEvent', +]; + +describe('AGMA Analytics Adapter', () => { + let agmaConfig, sandbox, clock; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + sandbox.stub(events, 'getEvents').returns([]); + agmaConfig = { + options: { + code: 'test', + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('configuration', () => { + it('registers itself with the adapter manager', () => { + const adapter = adapterManager.getAnalyticsAdapter('agma'); + expect(adapter).to.exist; + expect(adapter.gvlid).to.equal(1122); + }); + }); + + describe('getPayload', () => { + it('should use non extended payload with no consent info', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null) + const payload = getPayload([generateUUID()], { + code: 'test', + }); + + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use non extended payload when agma is not in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: false, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use extended payload when agma is in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...extendedKey, 'debug']); + }); + }); + + describe('getTiming', () => { + let originalPerformance; + let originalWindowPerformanceNow; + + beforeEach(() => { + originalPerformance = global.performance; + originalWindowPerformanceNow = window.performance.now; + }); + + afterEach(() => { + global.performance = originalPerformance; + window.performance.now = originalWindowPerformanceNow; + }); + + it('returns TTFB using Timing API V2', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 100, startTime: 50 }]), + now: sinon.stub().returns(150), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 50, elapsedTime: 150 }); + }); + + it('returns TTFB using Timing API V1 when V2 is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: { responseStart: 150, fetchStart: 50 }, + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 100, elapsedTime: 200 }); + }); + + it('returns null when Timing API is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: undefined, + }; + + const result = getTiming(); + + expect(result).to.be.null; + }); + + it('returns ttfb as 0 if calculated value is negative', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 150 }]), + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 200 }); + }); + + it('returns ttfb as 0 if calculated value exceeds performance.now()', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 0 }]), + now: sinon.stub().returns(40), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 40 }); + }); + }); + + describe('getOrtb2Data', () => { + it('returns site and user from options when available', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return {}; + }); + + const ortb2 = { + user: 'user', + site: 'site', + }; + + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal(ortb2); + }); + + it('returns a combination of data from options and pGlobal.readConfig', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return { + ortb2: { + site: { + foo: 'bar', + }, + }, + }; + }); + + const ortb2 = { + user: 'user', + }; + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal({ + site: { + foo: 'bar', + }, + user: 'user', + }); + }); + }); + + describe('Event Payload', () => { + beforeEach(() => { + agmaAnalyticsAdapter.enableAnalytics({ + ...agmaConfig, + }); + server.respondWith('POST', INGEST_URL, [ + 200, + { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + '', + ]); + }); + + afterEach(() => { + agmaAnalyticsAdapter.auctionIds = []; + if (agmaAnalyticsAdapter.timer) { + clearTimeout(agmaAnalyticsAdapter.timer); + } + agmaAnalyticsAdapter.disableAnalytics(); + }); + + it('should only send once per minute', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('1'), + auction, + }); + + clock.tick(200); + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('2'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('3'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('4'), + auction, + }); + + clock.tick(900); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + }); + + it('should send the extended payload with consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1100); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should send the non extended payload with no explicit consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + })); + + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should set the trigger Event', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null); + agmaAnalyticsAdapter.disableAnalytics(); + agmaAnalyticsAdapter.enableAnalytics({ + provider: 'agma', + options: { + code: 'test', + triggerEvent: constants.EVENTS.AUCTION_END + }, + }); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + events.emit(constants.EVENTS.AUCTION_END, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.auctionIds).to.have.length(1); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_END); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + }); +}); diff --git a/test/spec/modules/ajaBidAdapter_spec.js b/test/spec/modules/ajaBidAdapter_spec.js index 7cf5698f7d4..3137c9dc24e 100644 --- a/test/spec/modules/ajaBidAdapter_spec.js +++ b/test/spec/modules/ajaBidAdapter_spec.js @@ -62,8 +62,17 @@ describe('AjaAdapter', function () { model: 'SM-G955U', bitness: '64', architecture: '' + }, + ext: { + cdep: 'example_label_1' } } + }, + ortb2Imp: { + ext: { + tid: 'cea1eb09-d970-48dc-8585-634d3a7b0544', + gpid: '/1111/homepage#300x250' + } } } ]; @@ -78,7 +87,7 @@ describe('AjaAdapter', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); - expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&sua=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22Android%22%2C%22version%22%3A%5B%228%22%2C%220%22%2C%220%22%5D%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Not_A%20Brand%22%2C%22version%22%3A%5B%2299%22%2C%220%22%2C%220%22%2C%220%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%2C%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%5D%2C%22mobile%22%3A1%2C%22model%22%3A%22SM-G955U%22%2C%22bitness%22%3A%2264%22%2C%22architecture%22%3A%22%22%7D&'); + expect(requests[0].data).to.equal('asi=123456&skt=5&gpid=%2F1111%2Fhomepage%23300x250&tid=cea1eb09-d970-48dc-8585-634d3a7b0544&cdep=example_label_1&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&ad_format_ids=2&sua=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22Android%22%2C%22version%22%3A%5B%228%22%2C%220%22%2C%220%22%5D%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Not_A%20Brand%22%2C%22version%22%3A%5B%2299%22%2C%220%22%2C%220%22%2C%220%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%2C%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%5D%2C%22mobile%22%3A1%2C%22model%22%3A%22SM-G955U%22%2C%22bitness%22%3A%2264%22%2C%22architecture%22%3A%22%22%7D&'); }); }); @@ -116,7 +125,7 @@ describe('AjaAdapter', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); - expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&eids=%7B%22eids%22%3A%5B%7B%22source%22%3A%22pubcid.org%22%2C%22uids%22%3A%5B%7B%22id%22%3A%22some-random-id-value%22%2C%22atype%22%3A1%7D%5D%7D%5D%7D&'); + expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&ad_format_ids=2&eids=%7B%22eids%22%3A%5B%7B%22source%22%3A%22pubcid.org%22%2C%22uids%22%3A%5B%7B%22id%22%3A%22some-random-id-value%22%2C%22atype%22%3A1%7D%5D%7D%5D%7D&'); }); }); @@ -173,138 +182,6 @@ describe('AjaAdapter', function () { expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); - it('handles video responses', function () { - let response = { - 'is_ad_return': true, - 'ad': { - 'ad_type': 3, - 'prebid_id': '51ef8751f9aead', - 'price': 12.34, - 'currency': 'JPY', - 'creative_id': '123abc', - 'video': { - 'w': 300, - 'h': 250, - 'vtag': '', - 'purl': 'https://cdn/player', - 'progress': true, - 'loop': false, - 'inread': false, - 'adomain': [ - 'www.example.com' - ] - } - }, - 'syncs': [ - 'https://example.com' - ] - }; - - let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result[0]).to.have.property('vastXml'); - expect(result[0]).to.have.property('renderer'); - expect(result[0]).to.have.property('mediaType', 'video'); - }); - - it('handles native response', function () { - let response = { - 'is_ad_return': true, - 'ad': { - 'ad_type': 2, - 'prebid_id': '51ef8751f9aead', - 'price': 12.34, - 'currency': 'JPY', - 'creative_id': '123abc', - 'native': { - 'template_and_ads': { - 'head': '', - 'body_wrapper': '', - 'body': '', - 'ads': [ - { - 'ad_format_id': 10, - 'assets': { - 'ad_spot_id': '123abc', - 'index': 0, - 'adchoice_url': 'https://aja-kk.co.jp/optout', - 'cta_text': 'cta', - 'img_icon': 'https://example.com/img_icon', - 'img_icon_width': '50', - 'img_icon_height': '50', - 'img_main': 'https://example.com/img_main', - 'img_main_width': '200', - 'img_main_height': '100', - 'lp_link': 'https://example.com/lp?k=v', - 'sponsor': 'sponsor', - 'title': 'ad_title', - 'description': 'ad_desc' - }, - 'imps': [ - 'https://example.com/imp' - ], - 'inviews': [ - 'https://example.com/inview' - ], - 'jstracker': '', - 'disable_trimming': false, - 'adomain': [ - 'www.example.com' - ] - } - ] - } - } - }, - 'syncs': [ - 'https://example.com' - ] - }; - - let expectedResponse = [ - { - 'requestId': '51ef8751f9aead', - 'cpm': 12.34, - 'creativeId': '123abc', - 'dealId': undefined, - 'mediaType': 'native', - 'currency': 'JPY', - 'ttl': 300, - 'netRevenue': true, - 'native': { - 'title': 'ad_title', - 'body': 'ad_desc', - 'cta': 'cta', - 'sponsoredBy': 'sponsor', - 'image': { - 'url': 'https://example.com/img_main', - 'width': 200, - 'height': 100 - }, - 'icon': { - 'url': 'https://example.com/img_icon', - 'width': 50, - 'height': 50 - }, - 'clickUrl': 'https://example.com/lp?k=v', - 'impressionTrackers': [ - 'https://example.com/imp' - ], - 'privacyLink': 'https://aja-kk.co.jp/optout' - }, - 'meta': { - 'advertiserDomains': [ - 'www.example.com' - ] - } - } - ]; - - let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}) - expect(result).to.deep.equal(expectedResponse) - }); - it('handles nobid responses', function () { let response = { 'is_ad_return': false, diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index 08f00186358..90a9e409e69 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -68,7 +68,7 @@ const BIDDER_VIDEO_RESPONSE = { 'ttl': 200, 'creativeId': 2, 'netRevenue': true, - 'winUrl': 'http://test.com', + 'winUrl': 'http://test.com?price=${AUCTION_PRICE}', 'mediaType': 'video', 'adomain': ['test.com'] }] @@ -112,7 +112,15 @@ describe('alkimiBidAdapter', function () { vendorData: {}, gdprApplies: true }, - uspConsent: 'uspConsent' + uspConsent: 'uspConsent', + ortb2: { + site: { + keywords: 'test1, test2' + }, + at: 2, + bcat: ['BSW1', 'BSW2'], + wseat: ['16', '165'] + } } const bidderRequest = spec.buildRequests(bidRequests, requestData) @@ -138,6 +146,7 @@ describe('alkimiBidAdapter', function () { expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) + expect(bidderRequest.data.ortb2).to.deep.contains({ at: 2, wseat: ['16', '165'], bcat: ['BSW1', 'BSW2'], site: { keywords: 'test1, test2' }, }) expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) expect(bidderRequest.options.contentType).to.equal('application/json') expect(bidderRequest.url).to.equal(ENDPOINT) @@ -186,9 +195,9 @@ describe('alkimiBidAdapter', function () { expect(result[0]).to.have.property('ttl').equal(200) expect(result[0]).to.have.property('creativeId').equal(2) expect(result[0]).to.have.property('netRevenue').equal(true) - expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('winUrl').equal('http://test.com?price=${AUCTION_PRICE}') expect(result[0]).to.have.property('mediaType').equal('video') - expect(result[0]).to.have.property('vastXml').equal('vast') + expect(result[0]).to.have.property('vastUrl').equal('http://test.com?price=800.4') expect(result[0].meta).to.exist.property('advertiserDomains') expect(result[0].meta).to.have.property('advertiserDomains').lengthOf(1) }) diff --git a/test/spec/modules/ampliffyBidAdapter_spec.js b/test/spec/modules/ampliffyBidAdapter_spec.js new file mode 100644 index 00000000000..5b86f692d7e --- /dev/null +++ b/test/spec/modules/ampliffyBidAdapter_spec.js @@ -0,0 +1,453 @@ +import { + parseXML, + isAllowedToBidUp, + spec, + getDefaultParams, + mergeParams, + paramsToQueryString, setCurrentURL +} from 'modules/ampliffyBidAdapter.js'; +import {expect} from 'chai'; +import {BANNER, VIDEO} from 'src/mediaTypes'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('Ampliffy bid adapter Test', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + // Global definitions for all tests + const xmlStr = ` + + + + ]]> + + + + ES + `; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + let companion = xml.getElementsByTagName('Companion')[0]; + let htmlResource = companion.getElementsByTagName('HTMLResource')[0]; + let htmlContent = document.createElement('html'); + htmlContent.innerHTML = htmlResource.textContent; + + describe('Is allowed to bid up', function () { + it('Should return true using a URL that is in domainMap', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://testSports.com?id=131313&text=aaaaa&foo=foo'); + expect(allowedToBidUp).to.be.true; + }) + + it('Should return false using an url that is not in domainMap', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://test.com'); + expect(allowedToBidUp).to.be.false; + }) + + it('Should return false using an url that is excluded.', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://www.no-allowed.com/busqueda/sexo/sexo?test=1#item1'); + expect(allowedToBidUp).to.be.false; + }) + }) + + describe('Helper functions', function () { + it('Should default params not to be null', () => { + const defaultParams = getDefaultParams(); + + expect(defaultParams).not.to.be.null; + }) + it('Should the merge two object params into a new object', () => { + const params1 = { + 'hello': 'world', + 'ampTest': 'this will be replaced' + } + const params2 = { + 'test': 1, + 'ampTest': 'This will be replace the param with the same name in other array' + } + const allParams = mergeParams(params1, params2); + + const paramsComplete = + { + 'hello': 'world', + 'ampTest': 'This will be replace the param with the same name in other array', + 'test': 1, + } + expect(allParams).not.to.be.null; + expect(JSON.stringify(allParams)).to.equal(JSON.stringify(paramsComplete)); + }) + it('Params to QueryString', () => { + const params = { + 'test': 1, + 'ampTest': 'ret', + 'empty': null, + 'quoteMark': '?', + 'test1': undefined + } + const queryString = paramsToQueryString(params); + + expect(queryString).not.to.be.null; + expect(queryString).to.equal('test=1&Test=ret&empty"eMark=%3F'); + }) + }) + + describe('isBidRequestValid', function () { + it('Should return true when required params found', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return false when param format is display but mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'display' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return false when param format is video but mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'video' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return true when param format is video and mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'video' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is display and mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'display' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is all and mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is all and mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return false without placementId param', function () { + const bidRequest = { + bidder: 'ampliffy', + params: {} + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return false without param object', function () { + const bidRequest = { + bidder: 'ampliffy', + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + }); + + describe('Build request function', function () { + const bidderRequest = { + 'bidderCode': 'ampliffy', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'bidderRequestId': '1134bdcbe47f25', + 'bids': [{ + 'bidder': 'ampliffy', + 'params': { + 'placementId': 1235465798, + 'type': 'bidder.', + 'region': 'alan-development.k8s.', + 'adnetwork': 'ampliffy.com', + 'SERVER': 'bidder.ampliffy.com' + }, + 'crumbs': {'pubcid': '29844d69-c4e5-4b00-8602-6dd09815363a'}, + 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video1'}}}, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': 'f85c1b10-bad3-4c3f-a2bb-2c484c405bc9', + 'sizes': [[640, 480]], + 'bidId': '2bc71d9c058842', + 'bidderRequestId': '1134bdcbe47f25', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1644029483655, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'http://localhost:9999/integrationExamples/gpt/hello_world_video.html?pbjs_debug=true', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': ['http://localhost:9999/integrationExamples/gpt/hello_world_video.html?pbjs_debug=true'], + 'canonicalUrl': null + }, + 'start': 1644029483708 + } + const validBidRequests = [ + { + 'bidder': 'ampliffy', + 'params': { + 'placementId': 1235465798, + 'type': 'bidder.', + 'region': 'alan-development.k8s.', + 'adnetwork': 'ampliffy.com', + 'SERVER': 'bidder.ampliffy.com' + }, + 'crumbs': {'pubcid': '29844d69-c4e5-4b00-8602-6dd09815363a'}, + 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video1'}}}, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': 'f85c1b10-bad3-4c3f-a2bb-2c484c405bc9', + 'sizes': [[640, 480]], + 'bidId': '2bc71d9c058842', + 'bidderRequestId': '1134bdcbe47f25', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ]; + it('Should return one or more bid requests', function () { + expect(spec.buildRequests(validBidRequests, bidderRequest).length).to.be.greaterThan(0); + }); + }) + describe('Interpret response', function () { + let bidRequest = { + bidRequest: { + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: '469bb2e2-351f-4d01-b782-cdbca5e3e0ed', + bidId: '2d40b8dcd02ade', + bidRequestsCount: 1, + bidder: 'ampliffy', + bidderRequestId: '128c07edc4680f', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { + pubcid: '29844d69-c4e5-4b00-8602-6dd09815363a' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + ortb2Imp: {ext: {}}, + params: {placementId: 13144370}, + sizes: [ + [300, 250], + [300, 600] + ], + src: 'client', + transactionId: '103b2b58-6ed1-45e9-9486-c942d6042e3' + }, + data: {bidId: '2d40b8dcd02ade'}, + method: 'GET', + url: 'https://test.com', + }; + + it('Should extract a CPM and currency from the xml', () => { + let cpmData = parseXML(xml); + expect(cpmData).to.not.be.a('null'); + expect(cpmData.cpm).to.equal('.23'); + expect(cpmData.currency).to.equal('USD'); + }); + + it('It should return no ads when the CPM is less than zero.', () => { + const xmlStr1 = ` + + + + + + + + +
+
+
+
+ + + ]]> +
+
+ + ES +
+
+
`; + let serverResponse = { + 'body': xmlStr1, + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(0); + }) + + it('It should return no ads when the creative url is not in the xml', () => { + const xmlStr1 = ` + + + + + + + + +
+
+
+
+ + ]]> + + + ES + + + `; + let serverResponse = { + 'body': xmlStr1, + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(0); + }) + it('It should return a banner ad.', () => { + let serverResponse = { + 'body': xmlStr, + } + setCurrentURL('https://www.sports.com'); + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).greaterThan(0); + expect(bidResponses[0].mediaType).to.be.equal(BANNER); + expect(bidResponses[0].ad).not.to.be.null; + }) + it('It should return a video ad.', () => { + let serverResponse = { + 'body': xmlStr, + } + setCurrentURL('https://www.sports.com'); + bidRequest.bidRequest.mediaTypes = { + video: { + sizes: [ + [300, 250], + [300, 600] + ] + } + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).greaterThan(0); + expect(bidResponses[0].mediaType).to.be.equal(VIDEO); + expect(bidResponses[0].vastUrl).not.to.be.null; + }) + }); +}); diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 984c443344d..21fa2e2617c 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/amxBidAdapter.js'; import { createEidsArray } from 'modules/userId/eids.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; +import { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; const sampleRequestId = '82c91e127a9b93e'; @@ -11,7 +12,7 @@ const sampleDisplayCRID = '78827819'; // minimal example vast const sampleVideoAd = (addlImpression) => ` -00:00:15${addlImpression} +00:00:15${addlImpression} `.replace(/\n+/g, ''); const sampleFPD = { @@ -37,7 +38,7 @@ const sampleBidderRequest = { }, gppConsent: { gppString: 'example', - applicableSections: 'example' + applicableSections: 'example', }, auctionId: null, @@ -209,10 +210,12 @@ describe('AmxBidAdapter', () => { describe('getUserSync', () => { it('Will perform an iframe sync even if there is no server response..', () => { const syncs = spec.getUserSyncs({ iframeEnabled: true }); - expect(syncs).to.eql([{ - type: 'iframe', - url: 'https://prebid.a-mo.net/isyn?gdpr_consent=&gdpr=0&us_privacy=&gpp=&gpp_sid=' - }]); + expect(syncs).to.eql([ + { + type: 'iframe', + url: 'https://prebid.a-mo.net/isyn?gdpr_consent=&gdpr=0&us_privacy=&gpp=&gpp_sid=', + }, + ]); }); it('will return valid syncs from a server response', () => { @@ -276,8 +279,13 @@ describe('AmxBidAdapter', () => { }); it('will attach additional referrer info data', () => { - const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); - expect(data.ri.r).to.equal(sampleBidderRequest.refererInfo.topmostLocation); + const { data } = spec.buildRequests( + [sampleBidRequestBase], + sampleBidderRequest + ); + expect(data.ri.r).to.equal( + sampleBidderRequest.refererInfo.topmostLocation + ); expect(data.ri.t).to.equal(sampleBidderRequest.refererInfo.reachedTop); expect(data.ri.l).to.equal(sampleBidderRequest.refererInfo.numIframes); expect(data.ri.s).to.equal(sampleBidderRequest.refererInfo.stack); @@ -315,7 +323,7 @@ describe('AmxBidAdapter', () => { [sampleBidRequestBase], sampleBidderRequest ); - delete data.m; // don't deal with "m" in this test + delete data.m; // don't deal with 'm' in this test expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies); expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString); expect(data.usp).to.equal(sampleBidderRequest.uspConsent); @@ -343,10 +351,8 @@ describe('AmxBidAdapter', () => { }); it('will attach sync configuration', () => { - const request = () => spec.buildRequests( - [sampleBidRequestBase], - sampleBidderRequest - ); + const request = () => + spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); const setConfig = (filterSettings) => config.setConfig({ @@ -355,56 +361,73 @@ describe('AmxBidAdapter', () => { syncDelay: 2300, syncEnabled: true, filterSettings, - } + }, }); const test = (filterSettings) => { setConfig(filterSettings); return request().data.sync; - } + }; const base = { d: 2300, l: 2, e: true }; - const tests = [[ - undefined, - { ...base, t: 0 } - ], [{ - image: { - bidders: '*', - filter: 'include' - }, - iframe: { - bidders: '*', - filter: 'include' - } - }, { ...base, t: 3 }], [{ - image: { - bidders: ['amx'], - }, - iframe: { - bidders: '*', - filter: 'include' - } - }, { ...base, t: 3 }], [{ - image: { - bidders: ['other'], - }, - iframe: { - bidders: '*' - } - }, { ...base, t: 2 }], [{ - image: { - bidders: ['amx'] - }, - iframe: { - bidders: ['amx'], - filter: 'exclude' - } - }, { ...base, t: 1 }]] + const tests = [ + [undefined, { ...base, t: 0 }], + [ + { + image: { + bidders: '*', + filter: 'include', + }, + iframe: { + bidders: '*', + filter: 'include', + }, + }, + { ...base, t: 3 }, + ], + [ + { + image: { + bidders: ['amx'], + }, + iframe: { + bidders: '*', + filter: 'include', + }, + }, + { ...base, t: 3 }, + ], + [ + { + image: { + bidders: ['other'], + }, + iframe: { + bidders: '*', + }, + }, + { ...base, t: 2 }, + ], + [ + { + image: { + bidders: ['amx'], + }, + iframe: { + bidders: ['amx'], + filter: 'exclude', + }, + }, + { ...base, t: 1 }, + ], + ]; for (let i = 0, l = tests.length; i < l; i++) { const [result, expected] = tests[i]; - expect(test(result), `input: ${JSON.stringify(result)}`).to.deep.equal(expected); + expect(test(result), `input: ${JSON.stringify(result)}`).to.deep.equal( + expected + ); } }); @@ -497,7 +520,15 @@ describe('AmxBidAdapter', () => { it('can build a video request', () => { const { data } = spec.buildRequests( - [{ ...sampleBidRequestVideo, params: { ...sampleBidRequestVideo.params, adUnitId: 'custom-auid' } }], + [ + { + ...sampleBidRequestVideo, + params: { + ...sampleBidRequestVideo.params, + adUnitId: 'custom-auid', + }, + }, + ], sampleBidderRequest ); expect(Object.keys(data.m).length).to.equal(1); @@ -659,15 +690,49 @@ describe('AmxBidAdapter', () => { }); it('will log an event for timeout', () => { - spec.onTimeout({ - bidder: 'example', - bidId: 'test-bid-id', - adUnitCode: 'div-gpt-ad', - timeout: 300, - auctionId: utils.getUniqueIdentifierStr(), + // this will use sendBeacon.. + spec.onTimeout([ + { + bidder: 'example', + bidId: 'test-bid-id', + adUnitCode: 'div-gpt-ad', + ortb2: { + site: { + ref: 'https://example.com', + }, + }, + params: { + tagId: 'tag-id', + }, + timeout: 300, + auctionId: utils.getUniqueIdentifierStr(), + }, + ]); + + const [request] = server.requests; + request.respond(204, {'Content-Type': 'text/html'}, null); + expect(request.url).to.equal('https://1x1.a-mo.net/e'); + + if (typeof Request !== 'undefined' && 'keepalive' in Request.prototype) { + expect(request.fetch.request.keepalive).to.equal(true); + } + + const {c: common, e: events} = JSON.parse(request.requestBody) + expect(common).to.deep.equal({ + V: '$prebid.version$', + vg: '$$PREBID_GLOBAL$$', + U: null, + re: 'https://example.com', }); - expect(firedPixels.length).to.equal(1); - expect(firedPixels[0]).to.match(/\/hbx\/g_pbto/); + + expect(events.length).to.equal(1); + const [event] = events; + expect(event.n).to.equal('g_pbto') + expect(event.A).to.equal('example'); + expect(event.mid).to.equal('tag-id'); + expect(event.cn).to.equal(300); + expect(event.bid).to.equal('test-bid-id'); + expect(event.a).to.equal('div-gpt-ad'); }); it('will log an event for prebid win', () => { diff --git a/test/spec/modules/asteriobidAnalyticsAdapter_spec.js b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9be6c1dedac --- /dev/null +++ b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js @@ -0,0 +1,151 @@ +import asteriobidAnalytics, {storage} from 'modules/asteriobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {expectEvents} from '../../helpers/analytics.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('AsterioBid Analytics Adapter', function () { + let bidWonEvent = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': '1ebb82ec35375e', + 'mediaType': 'banner', + 'cpm': 0.5, + 'requestId': '1582271863760569973', + 'creative_id': '96846035', + 'creativeId': '96846035', + 'ttl': 60, + 'currency': 'USD', + 'netRevenue': true, + 'auctionId': '9c7b70b9-b6ab-4439-9e71-b7b382797c18', + 'responseTimestamp': 1537521629657, + 'requestTimestamp': 1537521629331, + 'bidder': 'appnexus', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 326, + 'size': '300x250', + 'status': 'rendered', + 'eventType': 'bidWon', + 'ad': 'some ad', + 'adUrl': 'ad url' + }; + + describe('AsterioBid Analytic tests', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + asteriobidAnalytics.disableAnalytics(); + events.getEvents.restore(); + }); + + it('support custom endpoint', function () { + let custom_url = 'custom url'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + url: custom_url, + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expect(asteriobidAnalytics.getOptions().url).to.equal(custom_url); + }); + + it('bid won event', function() { + let bundleId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: bundleId + } + }); + + events.emit(constants.EVENTS.BID_WON, bidWonEvent); + asteriobidAnalytics.flush(); + + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://endpt.asteriobid.com/endpoint'); + expect(server.requests[0].requestBody.substring(0, 2)).to.equal('1:'); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + expect(pmEvents.pageViewId).to.exist; + expect(pmEvents.bundleId).to.equal(bundleId); + expect(pmEvents.ver).to.equal(1); + expect(pmEvents.events.length).to.equal(1); + expect(pmEvents.events[0].eventType).to.equal('bidWon'); + expect(pmEvents.events[0].ad).to.be.undefined; + expect(pmEvents.events[0].adUrl).to.be.undefined; + }); + + it('track event without errors', function () { + sinon.spy(asteriobidAnalytics, 'track'); + + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expectEvents().to.beTrackedBy(asteriobidAnalytics.track); + }); + }); + + describe('build utm tag data', function () { + let getDataFromLocalStorageStub; + this.timeout(4000) + beforeEach(function () { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs('pm_utm_source').returns('utm_source'); + getDataFromLocalStorageStub.withArgs('pm_utm_medium').returns('utm_medium'); + getDataFromLocalStorageStub.withArgs('pm_utm_campaign').returns('utm_camp'); + getDataFromLocalStorageStub.withArgs('pm_utm_term').returns(''); + getDataFromLocalStorageStub.withArgs('pm_utm_content').returns(''); + }); + afterEach(function () { + getDataFromLocalStorageStub.restore(); + asteriobidAnalytics.disableAnalytics() + }); + it('should build utm data from local storage', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.utmTags.utm_source).to.equal('utm_source'); + expect(pmEvents.utmTags.utm_medium).to.equal('utm_medium'); + expect(pmEvents.utmTags.utm_campaign).to.equal('utm_camp'); + expect(pmEvents.utmTags.utm_term).to.equal(''); + expect(pmEvents.utmTags.utm_content).to.equal(''); + }); + }); + + describe('build page info', function () { + afterEach(function () { + asteriobidAnalytics.disableAnalytics() + }); + it('should build page info', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.pageInfo.domain).to.equal(window.location.hostname); + expect(pmEvents.pageInfo.referrerDomain).to.equal(utils.parseUrl(document.referrer).hostname); + }); + }); +}); diff --git a/test/spec/modules/axisBidAdapter_spec.js b/test/spec/modules/axisBidAdapter_spec.js new file mode 100644 index 00000000000..083f05f5c0a --- /dev/null +++ b/test/spec/modules/axisBidAdapter_spec.js @@ -0,0 +1,414 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/axisBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'axis' + +describe('AxisBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + integration: '000000', + token: '000000' + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + ortb2: { + site: { + cat: ['IAB24'] + } + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.axis-marketplace.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'iabCat', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.iabCat).to.have.lengthOf(1); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.integration).to.be.a('string'); + expect(placement.token).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + expect(placement.pos).to.be.within(0, 7); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + expect(placement.pos).to.be.within(0, 7); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + width: 300, + height: 250, + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta', 'width', 'height'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&ccpa=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js index f80051b0a50..f8e66caf657 100644 --- a/test/spec/modules/bizzclickBidAdapter_spec.js +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -1,6 +1,102 @@ import { expect } from 'chai'; -import { spec } from 'modules/bizzclickBidAdapter.js'; -import {config} from 'src/config.js'; +import { spec } from 'modules/bizzclickBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from '../../../src/config.js'; +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; + +// load modules that register ORTB processors +import 'src/prebid.js'; +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; + +const SIMPLE_BID_REQUEST = { + bidder: 'bizzclick', + params: { + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', + }, + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1499748733608-0', + transactionId: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + bidId: '33e9500b21129f', + bidderRequestId: '2772c1e566670b', + auctionId: '192721e36a0239', + sizes: [[300, 250], [160, 600]], + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', + }, +} + +const BANNER_BID_REQUEST = { + bidder: 'bizzclick', + params: { + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + code: 'banner_example', + timeout: 1000, +} + +const VIDEO_BID_REQUEST = { + placementCode: '/DfpAccount1/slotVideo', + bidId: 'test-bid-id-2', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + bidder: 'bizzclick', + params: { + accountId: '123', + sourceId: '123', + host: 'USE', + }, + adUnitCode: '/adunit-code/test-path', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, +} const NATIVE_BID_REQUEST = { code: 'native_example', @@ -34,386 +130,179 @@ const NATIVE_BID_REQUEST = { }, bidder: 'bizzclick', params: { - placementId: 'hash', - accountId: 'accountId' - }, - timeout: 1000 - -}; - -const BANNER_BID_REQUEST = { - code: 'banner_example', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - schain: { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'example.com', - sid: '164', - hp: 1 - } - ] - }, - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', timeout: 1000, - gdprConsent: { - consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', - gdprApplies: 1, - }, uspConsent: 'uspConsent' -} +}; -const bidRequest = { +const bidderRequest = { refererInfo: { - referer: 'test.com' - } -} - -const VIDEO_BID_REQUEST = { - code: 'video1', - sizes: [640, 480], - mediaTypes: { video: { - minduration: 0, - maxduration: 999, - boxingallowed: 1, - skip: 0, - mimes: [ - 'application/javascript', - 'video/mp4' - ], - w: 1920, - h: 1080, - protocols: [ - 2 - ], - linearity: 1, - api: [ - 1, - 2 - ] + page: 'https://publisher.com/home', + ref: 'https://referrer' } - }, - - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - }, - timeout: 1000 - -} - -const BANNER_BID_RESPONSE = { - id: 'request_id', - bidid: 'request_imp_id', - seatbid: [{ - bid: [{ - id: 'bid_id', - impid: 'request_imp_id', - price: 5, - adomain: ['example.com'], - adm: 'admcode', - crid: 'crid', - ext: { - mediaType: 'banner' - } - }], - }], -}; - -const VIDEO_BID_RESPONSE = { - id: 'request_id', - bidid: 'request_imp_id', - seatbid: [{ - bid: [{ - id: 'bid_id', - impid: 'request_imp_id', - price: 5, - adomain: ['example.com'], - adm: 'admcode', - crid: 'crid', - ext: { - mediaType: 'video', - vastUrl: 'http://example.vast', - } - }], - }], }; -let imgData = { - url: `https://example.com/image`, - w: 1200, - h: 627 -}; +const gdprConsent = { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', +} -const NATIVE_BID_RESPONSE = { - id: 'request_id', - bidid: 'request_imp_id', - seatbid: [{ - bid: [{ - id: 'bid_id', - impid: 'request_imp_id', - price: 5, - adomain: ['example.com'], - adm: { native: - { - assets: [ - {id: 0, title: 'dummyText'}, - {id: 3, image: imgData}, - { - id: 5, - data: {value: 'organization.name'} - } - ], - link: {url: 'example.com'}, - imptrackers: ['tracker1.com', 'tracker2.com', 'tracker3.com'], - jstracker: 'tracker1.com' - } - }, - crid: 'crid', - ext: { - mediaType: 'native' - } - }], - }], -}; +describe('bizzclickAdapter', function () { + const adapter = newBidder(spec); + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); -describe('BizzclickAdapter', function() { - describe('with COPPA', function() { - beforeEach(function() { + describe('with user privacy regulations', function () { + it('should send the Coppa "required" flag set to "1" in the request', function () { sinon.stub(config, 'getConfig') .withArgs('coppa') .returns(true); - }); - afterEach(function() { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(serverRequest.data.regs.coppa).to.equal(1); config.getConfig.restore(); }); - it('should send the Coppa "required" flag set to "1" in the request', function () { - let serverRequest = spec.buildRequests([BANNER_BID_REQUEST]); - expect(serverRequest.data[0].regs.coppa).to.equal(1); + it('should send the GDPR Consent data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({ ...bidderRequest, gdprConsent })); + expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); }); - }); - describe('isBidRequestValid', function() { - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(NATIVE_BID_REQUEST)).to.equal(true); - }); - - it('should return false when required params are not passed', function () { - let bid = Object.assign({}, NATIVE_BID_REQUEST); - delete bid.params; - bid.params = { - 'IncorrectParam': 0 - }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + it('should send the CCPA data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); }); }); - describe('build Native Request', function () { - const request = spec.buildRequests([NATIVE_BID_REQUEST], bidRequest); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(request).to.exist; - expect(request.method).to.exist; - expect(request.url).to.exist; - expect(request.data).to.exist; - }); - - it('sends bid request to our endpoint via POST', function () { - expect(request.method).to.equal('POST'); - }); - - it('Returns valid URL', function () { - expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(true); }); - it('Returns empty data if no valid requests are passed', function () { - let serverRequest = spec.buildRequests([]); - expect(serverRequest).to.be.an('array').that.is.empty; + it('should return false when accountID/sourceId is missing', function () { + let localbid = Object.assign({}, BANNER_BID_REQUEST); + delete localbid.params.accountId; + delete localbid.params.sourceId; + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(false); }); }); - describe('build Banner Request', function () { - const request = spec.buildRequests([BANNER_BID_REQUEST]); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(request).to.exist; - expect(request.method).to.exist; - expect(request.url).to.exist; - expect(request.data).to.exist; + describe('build request', function () { + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], syncAddFPDToBidderRequest(bidderRequest)); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); }); - it('sends bid request to our endpoint via POST', function () { + it('should return a valid bid request object', function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request).to.not.equal('array'); + expect(request.data).to.be.an('object'); expect(request.method).to.equal('POST'); + expect(request.url).to.not.equal(''); + expect(request.url).to.not.equal(undefined); + expect(request.url).to.not.equal(null); + + expect(request.data.site).to.have.property('page'); + expect(request.data.site).to.have.property('domain'); + expect(request.data).to.have.property('id'); + expect(request.data).to.have.property('imp'); + expect(request.data).to.have.property('device'); }); - it('check consent and ccpa string is set properly', function() { - expect(request.data[0].regs.ext.gdpr).to.equal(1); - expect(request.data[0].user.ext.consent).to.equal(BANNER_BID_REQUEST.gdprConsent.consentString); - expect(request.data[0].regs.ext.us_privacy).to.equal(BANNER_BID_REQUEST.uspConsent); - }) - - it('check schain is set properly', function() { - expect(request.data[0].source.ext.schain.complete).to.equal(1); - expect(request.data[0].source.ext.schain.ver).to.equal('1.0'); - }) - - it('Returns valid URL', function () { - expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + it('should return a valid bid BANNER request object', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); + expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); }); - }); - describe('build Video Request', function () { - const request = spec.buildRequests([VIDEO_BID_REQUEST]); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(request).to.exist; - expect(request.method).to.exist; - expect(request.url).to.exist; - expect(request.data).to.exist; - }); + if (FEATURES.VIDEO) { + it('should return a valid bid VIDEO request object', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.w).to.be.an('number'); + expect(request.data.imp[0].video.h).to.be.an('number'); + }); + } - it('sends bid request to our endpoint via POST', function () { - expect(request.method).to.equal('POST'); + it('should return a valid bid NATIVE request object', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0]).to.be.an('object'); }); + }) - it('Returns valid URL', function () { - expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + describe('interpretResponse', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [{ + 'bidId': '28ffdk2B952532', + 'bidder': 'bizzclick', + 'userId': { + 'freepassId': { + 'userIp': '172.21.0.1', + 'userId': '123', + 'commonId': 'commonIdValue' + } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' + } + }]; + bidderRequest = {}; }); - }); - describe('interpretResponse', function () { - it('Empty response must return empty array', function() { + it('Empty response must return empty array', function () { const emptyResponse = null; - let response = spec.interpretResponse(emptyResponse); + let response = spec.interpretResponse(emptyResponse, BANNER_BID_REQUEST); expect(response).to.be.an('array').that.is.empty; }) it('Should interpret banner response', function () { - const bannerResponse = { - body: [BANNER_BID_RESPONSE] - } - - const expectedBidResponse = { - requestId: BANNER_BID_RESPONSE.id, - cpm: BANNER_BID_RESPONSE.seatbid[0].bid[0].price, - width: BANNER_BID_RESPONSE.seatbid[0].bid[0].w, - height: BANNER_BID_RESPONSE.seatbid[0].bid[0].h, - ttl: BANNER_BID_RESPONSE.ttl || 1200, - currency: BANNER_BID_RESPONSE.cur || 'USD', - netRevenue: true, - creativeId: BANNER_BID_RESPONSE.seatbid[0].bid[0].crid, - dealId: BANNER_BID_RESPONSE.seatbid[0].bid[0].dealid, - - meta: {advertiserDomains: BANNER_BID_RESPONSE.seatbid[0].bid[0].adomain}, - mediaType: 'banner', - ad: BANNER_BID_RESPONSE.seatbid[0].bid[0].adm - } - - let bannerResponses = spec.interpretResponse(bannerResponse); - - expect(bannerResponses).to.be.an('array').that.is.not.empty; - let dataItem = bannerResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'meta', 'mediaType'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.ad).to.equal(expectedBidResponse.ad); - expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(expectedBidResponse.currency); - expect(dataItem.width).to.equal(expectedBidResponse.width); - expect(dataItem.height).to.equal(expectedBidResponse.height); - }); - - it('Should interpret video response', function () { - const videoResponse = { - body: [VIDEO_BID_RESPONSE] - } - - const expectedBidResponse = { - requestId: VIDEO_BID_RESPONSE.id, - cpm: VIDEO_BID_RESPONSE.seatbid[0].bid[0].price, - width: VIDEO_BID_RESPONSE.seatbid[0].bid[0].w, - height: VIDEO_BID_RESPONSE.seatbid[0].bid[0].h, - ttl: VIDEO_BID_RESPONSE.ttl || 1200, - currency: VIDEO_BID_RESPONSE.cur || 'USD', - netRevenue: true, - creativeId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid, - dealId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].dealid, - mediaType: 'video', - vastXml: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm, - meta: {advertiserDomains: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adomain}, - vastUrl: VIDEO_BID_RESPONSE.seatbid[0].bid[0].ext.vastUrl - } - - let videoResponses = spec.interpretResponse(videoResponse); - - expect(videoResponses).to.be.an('array').that.is.not.empty; - let dataItem = videoResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'meta', 'mediaType'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) - expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); - expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(expectedBidResponse.currency); - expect(dataItem.width).to.equal(expectedBidResponse.width); - expect(dataItem.height).to.equal(expectedBidResponse.height); - }); - - it('Should interpret native response', function () { - const nativeResponse = { - body: [NATIVE_BID_RESPONSE] - } - - const expectedBidResponse = { - requestId: NATIVE_BID_RESPONSE.id, - cpm: NATIVE_BID_RESPONSE.seatbid[0].bid[0].price, - width: NATIVE_BID_RESPONSE.seatbid[0].bid[0].w, - height: NATIVE_BID_RESPONSE.seatbid[0].bid[0].h, - ttl: NATIVE_BID_RESPONSE.ttl || 1200, - currency: NATIVE_BID_RESPONSE.cur || 'USD', - netRevenue: true, - creativeId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].crid, - dealId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].dealid, - mediaType: 'native', - meta: {advertiserDomains: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adomain}, - native: {clickUrl: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adm.native.link.url} - } - - let nativeResponses = spec.interpretResponse(nativeResponse); - - expect(nativeResponses).to.be.an('array').that.is.not.empty; - let dataItem = nativeResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - expect(dataItem.native.clickUrl).to.equal(expectedBidResponse.native.clickUrl) - expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); - expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(expectedBidResponse.currency); - expect(dataItem.width).to.equal(expectedBidResponse.width); - expect(dataItem.height).to.equal(expectedBidResponse.height); - }); + const serverResponse = { + body: { + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'impid': '28ffdk2B952532', + 'price': 97, + 'adm': '', + 'w': 300, + 'h': 250, + 'crid': 'creative0' + }] + }] + } + }; + it('should interpret server response', function () { + const bidRequest = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(serverResponse, bidRequest); + expect(bids).to.be.an('array'); + const bid = bids[0]; + expect(bid).to.be.an('object'); + expect(bid.currency).to.equal('USD'); + expect(bid.cpm).to.equal(97); + expect(bid.ad).to.equal(ad) + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('creative0'); + }); + }) }); -}) +}); diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index 8e96bd76940..3db97a17d88 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -1,5 +1,16 @@ -import { expect } from 'chai' -import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME } from 'modules/bliinkBidAdapter.js' +import { expect } from 'chai'; +import { + spec, + buildBid, + BLIINK_ENDPOINT_ENGINE, + getMetaList, + BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME, + getEffectiveConnectionType, + getUserIds, + getDomLoadingDuration, + GVL_ID, +} from 'modules/bliinkBidAdapter.js'; +import { config } from 'src/config.js'; /** * @description Mockup bidRequest @@ -20,6 +31,9 @@ import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_CO * crumbs: {pubcid: string}, * ortb2Imp: {ext: {data: {pbadslot: string}}}}} */ + +const connectionType = getEffectiveConnectionType(); +const domLoadingDuration = getDomLoadingDuration().toString(); const getConfigBid = (placement) => { return { adUnitCode: '/19968336/test', @@ -31,31 +45,33 @@ const getConfigBid = (placement) => { bidderRequestsCount: 1, bidderWinsCount: 0, crumbs: { - pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d' + pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d', }, sizes: [[300, 250]], mediaTypes: { banner: { - sizes: [ - [300, 250] - ] - } + sizes: [[300, 250]], + }, }, ortb2Imp: { ext: { data: { - pbadslot: '/19968336/test' - } - } + pbadslot: '/19968336/test', + }, + }, }, + domLoadingDuration, + ect: connectionType, params: { placement: placement, - tagId: '14f30eca-85d2-11e8-9eed-0242ac120007' + tagId: '14f30eca-85d2-11e8-9eed-0242ac120007', + videoUrl: 'https://www.example.com/advideo.mp4', + imageUrl: 'https://www.example.com/adimage.jpg', }, src: 'client', - transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf' - } -} + transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf', + }; +}; const getConfigBannerBid = () => { return { creative: { @@ -76,14 +92,13 @@ const getConfigBannerBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; const getConfigVideoBid = () => { return { creative: { video: { - content: - '', + content: '', height: 250, width: 300, }, @@ -99,8 +114,8 @@ const getConfigVideoBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; /** * @description Mockup response from engine.bliink.io/xxxx @@ -119,7 +134,7 @@ const getConfigVideoBid = () => { * } * } * } -* } + * } */ const getConfigCreative = () => { return { @@ -132,8 +147,8 @@ const getConfigCreative = () => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; const getConfigCreativeVideo = (isNoVast) => { return { @@ -147,8 +162,8 @@ const getConfigCreativeVideo = (isNoVast) => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; /** * @description Mockup BuildRequest function @@ -166,19 +181,19 @@ const getConfigBuildRequest = (placement) => { reachedTop: true, page: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', }, - } + }; if (!placement) { - return buildRequest + return buildRequest; } return Object.assign(buildRequest, { params: { bids: [getConfigBid(placement)], - placement: placement + placement: placement, }, - }) -} + }); +}; /** * @description Mockup response from API @@ -189,8 +204,8 @@ const getConfigInterpretResponse = (noAd = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } return { @@ -198,11 +213,12 @@ const getConfigInterpretResponse = (noAd = false) => { ...getConfigCreative(), mode: 'ad', transactionId: '2def0c5b2a7f6e', - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8' + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8', }, headers: {}, - } -} + }; +}; /** * @description Mockup response from API for RTB creative @@ -213,8 +229,8 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } const validVast = ` @@ -229,41 +245,43 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { - ` + `; const invalidVast = ` - ` + `; return { - body: { bids: [ - { - 'creative': { - 'video': { - 'content': isInvalidVast ? invalidVast : validVast, - 'height': 250, - 'width': 300 + body: { + bids: [ + { + creative: { + video: { + content: isInvalidVast ? invalidVast : validVast, + height: 250, + width: 300, + }, + media_type: 'video', + creativeId: 0, }, - 'media_type': 'video', - 'creativeId': 0, - }, - 'price': 0, - 'id': '8121', - 'token': 'token', - 'mode': 'rtb', - 'extras': { - 'deal_id': '34567ertyaza', - 'transaction_id': '2def0c5b2a7f6e' + price: 0, + id: '8121', + token: 'token', + mode: 'rtb', + extras: { + deal_id: '34567ertyaza', + transaction_id: '2def0c5b2a7f6e', + }, + currency: 'EUR', }, - 'currency': 'EUR' - } - ], - userSyncs: []} - } -} + ], + userSyncs: [], + }, + }; +}; /** * @@ -279,9 +297,9 @@ const testsGetMetaList = [ { title: 'Should return empty array if there are no parameters', args: { - fn: getMetaList() + fn: getMetaList(), }, - want: [] + want: [], }, { title: 'Should return list of metas with name associated', @@ -313,18 +331,63 @@ const testsGetMetaList = [ key: 'property', value: `'article:${'test'}'`, }, - ] - } -] + ], + }, +]; -describe('BLIINK Adapter getMetaList', function() { +describe('BLIINK Adapter getMetaList', function () { for (const test of testsGetMetaList) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); +const GetUserIds = [ + { + title: 'Should return undefined if there are no parameters', + args: { + fn: getUserIds(), + }, + want: undefined, + }, + { + title: 'Should return eids if exists', + args: { + fn: getUserIds([{ userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ] }]), + }, + want: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ], + }, +]; + +describe('BLIINK Adapter getUserIds', function () { + for (const test of GetUserIds) { + it(test.title, () => { + const res = test.args.fn; + expect(res).to.eql(test.want); + }); + } +}); /** * @description Array of tests used in describe function below @@ -349,127 +412,142 @@ const testsIsBidRequestValid = [ { title: 'isBidRequestValid format not valid', args: { - fn: spec.isBidRequestValid({}) + fn: spec.isBidRequestValid({}), }, want: false, }, { title: 'isBidRequestValid does not receive any bid', args: { - fn: spec.isBidRequestValid() + fn: spec.isBidRequestValid(), }, want: false, }, { title: 'isBidRequestValid Receive a valid bid', args: { - fn: spec.isBidRequestValid(getConfigBid('banner')) + fn: spec.isBidRequestValid(getConfigBid('banner')), }, want: true, - } -] + }, +]; -describe('BLIINK Adapter isBidRequestValid', function() { +describe('BLIINK Adapter isBidRequestValid', function () { for (const test of testsIsBidRequestValid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); -const vastXml = getConfigInterpretResponseRTB().body.bids[0].creative.video.content +const vastXml = + getConfigInterpretResponseRTB().body.bids[0].creative.video.content; const testsInterpretResponse = [ { title: 'Should construct bid for video instream', args: { - fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)) + fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)), }, - want: [{ - cpm: 0, - currency: 'EUR', - height: 250, - width: 300, - creativeId: '34567ertyaza', - mediaType: 'video', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(vastXml.replace(/\\"/g, '"')) - }] + want: [ + { + cpm: 0, + currency: 'EUR', + height: 250, + width: 300, + creativeId: '34567ertyaza', + mediaType: 'video', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + vastXml, + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(vastXml.replace(/\\"/g, '"')), + }, + ], }, { title: 'ServerResponse with message: invalid tag, return empty array', args: { - fn: spec.interpretResponse(getConfigInterpretResponse(true)) + fn: spec.interpretResponse(getConfigInterpretResponse(true)), }, - want: [] + want: [], }, { title: 'ServerResponse with mediaType banner', args: { - fn: spec.interpretResponse({body: {bids: [getConfigBannerBid()]}}), + fn: spec.interpretResponse({ body: { bids: [getConfigBannerBid()] } }), }, - want: [{ - ad: '', - cpm: 1, - creativeId: '34567erty', - currency: 'EUR', - height: 250, - mediaType: 'banner', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - width: 300 - }] + want: [ + { + ad: '', + cpm: 1, + creativeId: '34567erty', + currency: 'EUR', + height: 250, + mediaType: 'banner', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + width: 300, + }, + ], }, { title: 'ServerResponse with unhandled mediaType, return empty array', args: { - fn: spec.interpretResponse({body: {bids: [{...getConfigBannerBid(), - creative: { - unknown: { - adm: '', - height: 250, - width: 300, - }, - media_type: 'unknown', - creativeId: 125, - requestId: '2def0c5b2a7f6e', - }}]}}), + fn: spec.interpretResponse({ + body: { + bids: [ + { + ...getConfigBannerBid(), + creative: { + unknown: { + adm: '', + height: 250, + width: 300, + }, + media_type: 'unknown', + creativeId: 125, + requestId: '2def0c5b2a7f6e', + }, + }, + ], + }, + }), }, - want: [] + want: [], }, -] +]; -describe('BLIINK Adapter interpretResponse', function() { +describe('BLIINK Adapter interpretResponse', function () { for (const test of testsInterpretResponse) { it(test.title, () => { - const res = test.args.fn + const res = test.args.fn; if (res) { - expect(res).to.eql(test.want) + expect(res).to.eql(test.want); } - }) + }); } -}) +}); /** * @description Array of tests used in describe function below * @type {[ * {args: * {fn: { - * cpm: number, - * netRevenue: boolean, - * ad, requestId, - * meta: {mediaType}, - * width: number, - * currency: string, - * ttl: number, - * creativeId: number, - * height: number + * cpm: number, + * netRevenue: boolean, + * ad, requestId, + * meta: {mediaType}, + * width: number, + * currency: string, + * ttl: number, + * creativeId: number, + * height: number * } * }, want, title: string}]} */ @@ -478,21 +556,26 @@ const testsBuildBid = [ { title: 'Should return null if no bid passed in parameters', args: { - fn: buildBid() + fn: buildBid(), }, - want: null + want: null, }, { title: 'Input data must respect the output model', args: { - fn: buildBid({ id: 1, test: '123' }, { id: 2, test: '345' }, false, false) + fn: buildBid( + { id: 1, test: '123' }, + { id: 2, test: '345' }, + false, + false + ), }, - want: null + want: null, }, { title: 'input data respect the output model for video', args: { - fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()) + fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()), }, want: { requestId: getConfigBid('video').bidId, @@ -504,25 +587,31 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'use default height width output model for video', args: { - fn: buildBid({...getConfigVideoBid('video'), - creative: { - video: { - content: - '', - height: null, - width: null, + fn: buildBid( + { + ...getConfigVideoBid('video'), + creative: { + video: { + content: '', + height: null, + width: null, + }, + media_type: 'video', + creativeId: getConfigVideoBid().extras.deal_id, + requestId: '2def0c5b2a7f6e', }, - media_type: 'video', - creativeId: getConfigVideoBid().extras.deal_id, - requestId: '2def0c5b2a7f6e', - }}, getConfigCreativeVideo()) + }, + getConfigCreativeVideo() + ), }, want: { requestId: getConfigBid('video').bidId, @@ -534,14 +623,16 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'input data respect the output model for banner', args: { - fn: buildBid(getConfigBannerBid()) + fn: buildBid(getConfigBannerBid()), }, want: { requestId: getConfigBid('banner').bidId, @@ -554,18 +645,18 @@ const testsBuildBid = [ ad: getConfigBannerBid().creative.banner.adm, ttl: 300, netRevenue: true, - } - } -] + }, + }, +]; -describe('BLIINK Adapter buildBid', function() { +describe('BLIINK Adapter buildBid', function () { for (const test of testsBuildBid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); /** * @description Array of tests used in describe function below @@ -575,28 +666,33 @@ const testsBuildRequests = [ { title: 'Should not build request, no bidder request exist', args: { - fn: spec.buildRequests() + fn: spec.buildRequests(), }, - want: null + want: null, }, { title: 'Should build request if bidderRequest exist', args: { - fn: spec.buildRequests([], getConfigBuildRequest('banner')) + fn: spec.buildRequests([], getConfigBuildRequest('banner')), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, keywords: '', pageDescription: '', pageTitle: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -605,35 +701,43 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, }, { title: 'Should build request width GDPR configuration', args: { - fn: spec.buildRequests([], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -642,52 +746,115 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, + }, + { + title: 'Should build request width uspConsent if exists', + args: { + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + uspConsent: 'uspConsent', + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + uspConsent: 'uspConsent', + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, }, { title: 'Should build request width schain if exists', args: { - fn: spec.buildRequests([{schain: { - ver: '1.0', - complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] - }}], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [ + { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], + }, + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', schain: { ver: '1.0', complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], }, tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -696,107 +863,313 @@ const testsBuildRequests = [ }, ], }, - ] - } - } - } -] + ], + }, + }, + }, + { + title: 'Should build request with eids if exists', + args: { + fn: spec.buildRequests( + [ + { + userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + eids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, + }, +]; -describe('BLIINK Adapter buildRequests', function() { +describe('BLIINK Adapter buildRequests', function () { for (const test of testsBuildRequests) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + test.args.after; + }); } -}) +}); const getSyncOptions = (pixelEnabled = true, iframeEnabled = false) => { return { pixelEnabled, - iframeEnabled - } -} + iframeEnabled, + }; +}; const getServerResponses = () => { return [ { - body: {bids: [], - userSyncs: [ { - type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' - }, - { - type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - }]}, - } - ] -} + body: { + bids: [], + userSyncs: [ + { + type: 'script', + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', + }, + { + type: 'image', + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], + }, + }, + ]; +}; const getGdprConsent = () => { return { gdprApplies: 1, consentString: 'XXX', - apiVersion: 2 - } -} + apiVersion: 2, + }; +}; const testsGetUserSyncs = [ { title: 'Should not have gdprConsent exist', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', }, { type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - } - ] + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], }, { title: 'Should return iframe cookie sync if iframeEnabled', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${ + getGdprConsent().apiVersion + }`, }, - ] + ], }, { title: 'ccpa', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent(), 'ccpa-consent') + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent(), + 'ccpa-consent' + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&uspConsent=ccpa-consent&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&uspConsent=ccpa-consent&gdprConsent=${ + getGdprConsent().consentString + }&apiVersion=${getGdprConsent().apiVersion}`, }, - ] + ], }, { title: 'Should output sync if no gdprConsent', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()) + fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()), }, - want: getServerResponses()[0].body.userSyncs - } -] + want: getServerResponses()[0].body.userSyncs, + }, + { + title: 'Should output empty array if no pixelEnabled', + args: { + fn: spec.getUserSyncs({}, getServerResponses()), + }, + want: [], + }, +]; -describe('BLIINK Adapter getUserSyncs', function() { +describe('BLIINK Adapter getUserSyncs', function () { for (const test of testsGetUserSyncs) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); + +describe('BLIINK Adapter keywords & coppa true', function () { + it('Should build request with keyword and coppa true if exist', () => { + const metaElement = document.createElement('meta'); + metaElement.name = 'keywords'; + metaElement.content = 'Bliink, Saber, Prebid'; + sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const querySelectorStub = sinon + .stub(document, 'querySelector') + .returns(metaElement); + expect( + spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ) + ).to.eql({ + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + coppa: 1, + gdprConsent: 'XXXX', + pageDescription: 'Bliink, Saber, Prebid', + pageTitle: '', + keywords: 'Bliink,Saber,Prebid', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }); + querySelectorStub.restore(); + config.getConfig.restore(); + }); +}); + +describe('getEffectiveConnectionType', () => { + let navigatorStub; + + beforeEach(() => { + if ('connection' in navigator) { + navigatorStub = sinon.stub(navigator, 'connection').value({ + effectiveType: undefined, + }); + } + }); + + afterEach(() => { + if (navigatorStub) { + navigatorStub.restore(); + } + }); + if (navigatorStub) { + it('should return "unsupported" when effective connection type is undefined', () => { + const result = getEffectiveConnectionType(); + expect(result).to.equal('unsupported'); + }); + } +}); + +it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(GVL_ID); +}); diff --git a/test/spec/modules/boldwinBidAdapter_spec.js b/test/spec/modules/boldwinBidAdapter_spec.js index 5b51183ea6d..9a7b16c0914 100644 --- a/test/spec/modules/boldwinBidAdapter_spec.js +++ b/test/spec/modules/boldwinBidAdapter_spec.js @@ -19,7 +19,8 @@ describe('BoldwinBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -110,6 +111,36 @@ describe('BoldwinBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -283,7 +314,7 @@ describe('BoldwinBidAdapter', function () { expect(userSync[0].type).to.exist; expect(userSync[0].url).to.exist; expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://cs.videowalldirect.com'); + expect(userSync[0].url).to.be.equal('https://sync.videowalldirect.com'); }); }); }); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 75120aa7505..5fcc78f4322 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -89,12 +89,6 @@ describe('browsi Real time data sub module', function () { expect(browsiRTD.browsiSubmodule.getTargetingData([], null, null, auction)).to.eql({}); }); - it('should return NA if no prediction for ad unit', function () { - makeSlot({code: 'adMock', divId: 'browsiAd_2'}); - browsiRTD.setData({}); - expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'], null, null, auction)).to.eql({adMock: {bv: 'NA'}}); - }); - it('should return prediction from server', function () { makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); const data = { diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index 64f3d047a3a..3ccb5405552 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -237,6 +237,11 @@ describe('cadent_aperture_mx Adapter', function () { 'bidId': '30b31c2501de1e', 'auctionId': 'e19f1eff-8b27-42a6-888d-9674e5a6130c', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'ortb2Imp': { + 'ext': { + 'tid': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ed', + }, + }, }] }; let request = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -297,12 +302,22 @@ describe('cadent_aperture_mx Adapter', function () { expect(data.id).to.equal(bidderRequest.auctionId); expect(data.imp.length).to.equal(1); expect(data.imp[0].id).to.equal('30b31c2501de1e'); - expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ec'); + expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ed'); expect(data.imp[0].tagid).to.equal('25251'); expect(data.imp[0].secure).to.equal(0); expect(data.imp[0].vastXml).to.equal(undefined); }); + it('populates id even when auctionId is not available', function () { + // addressing https://github.com/prebid/Prebid.js/issues/9781 + bidderRequest.auctionId = null; + request = spec.buildRequests(bidderRequest.bids, bidderRequest); + + const data = JSON.parse(request.data); + expect(data.id).not.to.be.null; + expect(data.id).not.to.equal(bidderRequest.auctionId); + }); + it('properly sends site information and protocol', function () { request = spec.buildRequests(bidderRequest.bids, bidderRequest); request = JSON.parse(request.data); @@ -834,5 +849,38 @@ describe('cadent_aperture_mx Adapter', function () { expect(syncs[0].url).to.contains('usp=test'); expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test') }); + + it('should pass gpp string and section id', function() { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, {}, {}, { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs[0].url).to.contains('gpp=abcdefgs') + expect(syncs[0].url).to.contains('gpp_sid=1,2,4') + }); + + it('should pass us_privacy and gdpr string and gpp string', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, + { + gdprApplies: true, + consentString: 'test' + }, + { + consentString: 'test' + }, + { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + } + ); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('gdpr=1'); + expect(syncs[0].url).to.contains('usp=test'); + expect(syncs[0].url).to.contains('gpp=abcdefgs'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test&gpp=abcdefgs&gpp_sid=1,2,4'); + }); }); }); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 4a6a4f2ba60..0a76ed3e62d 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -94,7 +94,7 @@ describe('ConcertAdapter', function () { }); describe('spec.isBidRequestValid', function() { - it('should return when it recieved all the required params', function() { + it('should return when it received all the required params', function() { const bid = bidRequests[0]; expect(spec.isBidRequestValid(bid)).to.equal(true); }); @@ -116,7 +116,20 @@ describe('ConcertAdapter', function () { expect(payload).to.have.property('meta'); expect(payload).to.have.property('slots'); - const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent', 'browserLanguage']; + const metaRequiredFields = [ + 'prebidVersion', + 'pageUrl', + 'screen', + 'debug', + 'uid', + 'optedOut', + 'adapterVersion', + 'uspConsent', + 'gdprConsent', + 'gppConsent', + 'browserLanguage', + 'tdid' + ]; const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; metaRequiredFields.forEach(function(field) { @@ -199,6 +212,31 @@ describe('ConcertAdapter', function () { expect(slot.offsetCoordinates.x).to.equal(100) expect(slot.offsetCoordinates.y).to.equal(0) }) + + it('should not pass along tdid if the user has opted out', function() { + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should not pass along tdid if USP consent disallows', function() { + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, { ...bidRequest, uspConsent: '1YY' }); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should pass along tdid if the user has not opted out', function() { + storage.removeDataFromLocalStorage('c_nap', 'true'); + const tdid = '123abc'; + const bidRequestsWithTdid = [{ ...bidRequests[0], userId: { tdid } }] + const request = spec.buildRequests(bidRequestsWithTdid, bidRequest); + const payload = JSON.parse(request.data); + expect(payload.meta.tdid).to.equal(tdid); + }); }); describe('spec.interpretResponse', function() { diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js index 16ead9f9458..78f6a9d410d 100644 --- a/test/spec/modules/connatixBidAdapter_spec.js +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -135,14 +135,12 @@ describe('connatixBidAdapter', function () { describe('interpretResponse', function () { const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; - const Bid = {Cpm: 0.1, LineItems: [], RequestId: '2f897340c4eaa3', Ttl: 86400}; + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; let serverResponse; this.beforeEach(function () { serverResponse = { body: { - CustomerId, - PlayerId, Bids: [ Bid ] }, headers: function() { } @@ -162,18 +160,6 @@ describe('connatixBidAdapter', function () { expect(response).to.be.an('array').that.is.empty; }); - it('Should return an empty array if CustomerId is null', function () { - serverResponse.body.CustomerId = null; - const response = spec.interpretResponse(serverResponse); - expect(response).to.be.an('array').that.is.empty; - }); - - it('Should return an empty array if PlayerId is null', function () { - serverResponse.body.PlayerId = null; - const response = spec.interpretResponse(serverResponse); - expect(response).to.be.an('array').that.is.empty; - }); - it('Should return one bid response for one bid', function() { const bidResponses = spec.interpretResponse(serverResponse); expect(bidResponses.length).to.equal(1); @@ -212,12 +198,10 @@ describe('connatixBidAdapter', function () { const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; const UserSyncEndpoint = 'https://connatix.com/sync' - const Bid = {Cpm: 0.1, LineItems: [], RequestId: '2f897340c4eaa3', Ttl: 86400}; + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; const serverResponse = { body: { - CustomerId, - PlayerId, UserSyncEndpoint, Bids: [ Bid ] }, diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index 5376ba60886..686c3d63a63 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -3,6 +3,7 @@ import {connectIdSubmodule, storage} from 'modules/connectIdSystem.js'; import {server} from '../../mocks/xhr'; import {parseQS, parseUrl} from 'src/utils.js'; import {uspDataHandler, gppDataHandler} from 'src/adapterManager.js'; +import * as refererDetection from '../../../src/refererDetection'; const TEST_SERVER_URL = 'http://localhost:9876/'; @@ -288,6 +289,79 @@ describe('Yahoo ConnectID Submodule', () => { expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); }); + it('returns an object with the stored ID from cookies and syncs because of expired TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 10000; + const cookieData = {connectId: 'foo', he: 'email', lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL with provided puid', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + puid: '9' + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and syncs because is O&O traffic', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + const getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + getRefererInfoStub.returns({ + ref: 'https://dev.fc.yahoo.com?test' + }); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + getRefererInfoStub.restore(); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + it('Makes an ajax GET request to the production API endpoint with stored puid when id is stale', () => { const last15Days = Date.now() - (60 * 60 * 24 * 1000 * 15); const last29Days = Date.now() - (60 * 60 * 24 * 1000 * 29); diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index e15ce30940c..93a876d0233 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -290,7 +290,7 @@ describe('consentManagementGpp', function () { }); it('should not re-use errors', (done) => { - cmpResult = Promise.reject(new Error()); + cmpResult = GreedyPromise.reject(new Error()); GPPClient.init(makeCmp).catch(() => { cmpResult = {signalStatus: 'ready'}; return GPPClient.init(makeCmp).then(([client]) => { @@ -572,6 +572,17 @@ describe('consentManagementGpp', function () { expect(err.message).to.eql('err'); done(); }); + }); + + it('should not choke if supportedAPIs is missing', () => { + [gppData, pingData].forEach(ob => { delete ob.supportedAPIs; }) + mockCmpCommands({ + getGPPData: () => gppData + }); + return gppClient.getGPPData(pingData).then(res => { + expect(res.gppString).to.eql(gppData.gppString); + expect(res.parsedSections).to.eql({}); + }) }) describe('section data', () => { diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index e98486754ab..c372c66f7f0 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -522,6 +522,19 @@ describe('consentManagement', function () { setConsentConfig(goodConfig); expect(uspDataHandler.getConsentData()).to.eql('string'); }); + + it('does not invoke registerDeletion if the CMP calls back with an error', () => { + sandbox.stub(window, '__uspapi').callsFake((cmd, _, cb) => { + if (cmd === 'registerDeletion') { + cb(null, false); + } else { + // eslint-disable-next-line standard/no-callback-literal + cb({uspString: 'string'}, true); + } + }); + setConsentConfig(goodConfig); + sinon.assert.notCalled(adapterManager.callDataDeletionRequest); + }) }); }); }); diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index deeb8f7100d..d8e75454245 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -651,21 +651,30 @@ describe('Consumable BidAdapter', function () { expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=0&gdpr_consent=GDPR_CONSENT_STRING'); }) - it('should return a sync url if iframe syncs are enabled and GPP applies', function () { + it('should return a sync url if iframe syncs are enabled and has GPP consent with applicable sections', function () { let gppConsent = { applicableSections: [1, 2], gppString: 'GPP_CONSENT_STRING' } - let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, {}, gppConsent); + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); expect(opts.length).to.equal(1); - expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING&gpp_sid=1,2'); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING&gpp_sid=1%2C2'); }) - it('should return a sync url if iframe syncs are enabled and USP applies', function () { - let uspConsent = { - consentString: 'USP_CONSENT_STRING', + it('should return a sync url if iframe syncs are enabled and has GPP consent without applicable sections', function () { + let gppConsent = { + applicableSections: [], + gppString: 'GPP_CONSENT_STRING' } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled and USP applies', function () { + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, uspConsent); expect(opts.length).to.equal(1); @@ -677,9 +686,7 @@ describe('Consumable BidAdapter', function () { consentString: 'GDPR_CONSENT_STRING', gdprApplies: true, } - let uspConsent = { - consentString: 'USP_CONSENT_STRING', - } + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent, uspConsent); expect(opts.length).to.equal(1); @@ -704,50 +711,22 @@ describe('Consumable BidAdapter', function () { sandbox.restore(); }); - it('Request should have unifiedId config params', function() { + it('Request should have EIDs', function() { bidderRequest.bidRequest[0].userId = {}; bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); - let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ + bidderRequest.bidRequest[0].userIdAsEids = [{ 'source': 'adserver.org', 'uids': [{ - 'id': 'TTD_ID', + 'id': 'TTD_ID_FROM_USER_ID_MODULE', 'atype': 1, 'ext': { 'rtiPartner': 'TDID' } }] - }]); - }); - - it('Request should have adsrvrOrgId from UserId Module if config and userId module both have TTD ID', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - adsrvrOrgId: { - 'TDID': 'TTD_ID_FROM_CONFIG', - 'TDID_LOOKUP': 'TRUE', - 'TDID_CREATED_AT': '2022-06-21T09:47:00' - } - }; - return config[key]; - }); - bidderRequest.bidRequest[0].userId = {}; - bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); + }]; let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]); + expect(data.user.eids).to.deep.equal(bidderRequest.bidRequest[0].userIdAsEids); }); it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { diff --git a/test/spec/modules/contxtfulRtdProvider_spec.js b/test/spec/modules/contxtfulRtdProvider_spec.js new file mode 100644 index 00000000000..541c0e6e6dd --- /dev/null +++ b/test/spec/modules/contxtfulRtdProvider_spec.js @@ -0,0 +1,200 @@ +import { contxtfulSubmodule } from '../../../modules/contxtfulRtdProvider.js'; +import { expect } from 'chai'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +import * as events from '../../../src/events'; + +const _ = null; +const VERSION = 'v1'; +const CUSTOMER = 'CUSTOMER'; +const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/p.js`; +const INITIAL_RECEPTIVITY = { ReceptivityState: 'INITIAL_RECEPTIVITY' }; +const INITIAL_RECEPTIVITY_EVENT = new CustomEvent('initialReceptivity', { detail: INITIAL_RECEPTIVITY }); + +const CONTXTFUL_API = { GetReceptivity: sinon.stub() } +const RX_ENGINE_IS_READY_EVENT = new CustomEvent('rxEngineIsReady', {detail: CONTXTFUL_API}); + +function buildInitConfig(version, customer) { + return { + name: 'contxtful', + params: { + version, + customer, + }, + }; +} + +describe('contxtfulRtdProvider', function () { + let sandbox = sinon.sandbox.create(); + let loadExternalScriptTag; + let eventsEmitSpy; + + beforeEach(() => { + loadExternalScriptTag = document.createElement('script'); + loadExternalScriptStub.callsFake((_url, _moduleName) => loadExternalScriptTag); + + CONTXTFUL_API.GetReceptivity.reset(); + + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + afterEach(function () { + delete window.Contxtful; + sandbox.restore(); + }); + + describe('extractParameters with invalid configuration', () => { + const { + params: { customer, version }, + } = buildInitConfig(VERSION, CUSTOMER); + const theories = [ + [ + null, + 'params.version should be a non-empty string', + 'null object for config', + ], + [ + {}, + 'params.version should be a non-empty string', + 'empty object for config', + ], + [ + { customer }, + 'params.version should be a non-empty string', + 'customer only in config', + ], + [ + { version }, + 'params.customer should be a non-empty string', + 'version only in config', + ], + [ + { customer, version: '' }, + 'params.version should be a non-empty string', + 'empty string for version', + ], + [ + { customer: '', version }, + 'params.customer should be a non-empty string', + 'empty string for customer', + ], + [ + { customer: '', version: '' }, + 'params.version should be a non-empty string', + 'empty string for version & customer', + ], + ]; + + theories.forEach(([params, expectedErrorMessage, _description]) => { + const config = { name: 'contxtful', params }; + it('throws the expected error', () => { + expect(() => contxtfulSubmodule.extractParameters(config)).to.throw( + expectedErrorMessage + ); + }); + }); + }); + + describe('initialization with invalid config', function () { + it('returns false', () => { + expect(contxtfulSubmodule.init({})).to.be.false; + }); + }); + + describe('initialization with valid config', function () { + it('returns true when initializing', () => { + const config = buildInitConfig(VERSION, CUSTOMER); + expect(contxtfulSubmodule.init(config)).to.be.true; + }); + + it('loads contxtful module script asynchronously', (done) => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + + setTimeout(() => { + expect(loadExternalScriptStub.calledOnce).to.be.true; + expect(loadExternalScriptStub.args[0][0]).to.equal( + CONTXTFUL_CONNECTOR_ENDPOINT + ); + done(); + }, 10); + }); + }); + + describe('load external script return falsy', function () { + it('returns true when initializing', () => { + loadExternalScriptStub.callsFake(() => {}); + const config = buildInitConfig(VERSION, CUSTOMER); + expect(contxtfulSubmodule.init(config)).to.be.true; + }); + }); + + describe('rxEngine from external script', function () { + it('use rxEngine api to get receptivity', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(RX_ENGINE_IS_READY_EVENT); + + contxtfulSubmodule.getTargetingData(['ad-slot']); + + expect(CONTXTFUL_API.GetReceptivity.calledOnce).to.be.true; + }); + }); + + describe('initial receptivity is not dispatched', function () { + it('does not initialize receptivity value', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); + expect(targetingData).to.deep.equal({}); + }); + }); + + describe('initial receptivity is invalid', function () { + const theories = [ + [new Event('initialReceptivity'), 'event without details'], + [new CustomEvent('initialReceptivity', { }), 'custom event without details'], + [new CustomEvent('initialReceptivity', { detail: {} }), 'custom event with invalid details'], + [new CustomEvent('initialReceptivity', { detail: { ReceptivityState: '' } }), 'custom event with details without ReceptivityState'], + ]; + + theories.forEach(([initialReceptivityEvent, _description]) => { + it('does not initialize receptivity value', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(initialReceptivityEvent); + + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); + expect(targetingData).to.deep.equal({}); + }); + }) + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + { 'ad-slot': { ReceptivityState: 'INITIAL_RECEPTIVITY' } }, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + 'ad-slot-1': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + 'ad-slot-2': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, _description]) => { + it('adds "ReceptivityState" to the adUnits', function () { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(INITIAL_RECEPTIVITY_EVENT); + + expect(contxtfulSubmodule.getTargetingData(adUnits)).to.deep.equal( + expected + ); + }); + }); + }); +}); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 7cba0e2fbdf..5dc6d74d90e 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -364,93 +364,6 @@ describe('The Criteo bidding adapter', function () { }); it('should return false when given an invalid video bid request', function () { - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'instream', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 2, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'outstream', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'adpod', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - expect(spec.isBidRequestValid({ bidder: 'criteo', mediaTypes: { @@ -1122,6 +1035,30 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user.uspIab).to.equal('1YNY'); }); + it('should properly build a request with site and app ortb fields', function () { + const bidRequests = []; + let app = { + publisher: { + id: 'appPublisherId' + } + }; + let site = { + publisher: { + id: 'sitePublisherId' + } + }; + const bidderRequest = { + ortb2: { + app: app, + site: site + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.app).to.equal(app); + expect(request.data.site).to.equal(site); + }); + it('should properly build a request with device sua field', function () { const sua = {} const bidRequests = [ @@ -1326,12 +1263,25 @@ describe('The Criteo bidding adapter', function () { sizes: [[640, 480]], mediaTypes: { video: { + context: 'instream', playerSize: [640, 480], mimes: ['video/mp4', 'video/x-flv'], maxduration: 30, api: [1, 2], protocols: [2, 3], - plcmt: 3 + plcmt: 3, + w: 640, + h: 480, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], } }, params: { @@ -1350,6 +1300,7 @@ describe('The Criteo bidding adapter', function () { expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; + expect(ortbRequest.slots[0].video.context).to.equal('instream'); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480']); @@ -1362,6 +1313,18 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([1, 3]); expect(ortbRequest.slots[0].video.placement).to.equal(2); expect(ortbRequest.slots[0].video.plcmt).to.equal(3); + expect(ortbRequest.slots[0].video.w).to.equal(640); + expect(ortbRequest.slots[0].video.h).to.equal(480); + expect(ortbRequest.slots[0].video.linearity).to.equal(1); + expect(ortbRequest.slots[0].video.skipmin).to.equal(30); + expect(ortbRequest.slots[0].video.skipafter).to.equal(30); + expect(ortbRequest.slots[0].video.minbitrate).to.equal(10000); + expect(ortbRequest.slots[0].video.maxbitrate).to.equal(48000); + expect(ortbRequest.slots[0].video.delivery).to.deep.equal([1, 2, 3]); + expect(ortbRequest.slots[0].video.pos).to.equal(1); + expect(ortbRequest.slots[0].video.playbackend).to.equal(1); + expect(ortbRequest.slots[0].video.adPodDurationSec).to.equal(30); + expect(ortbRequest.slots[0].video.durationRangeSec).to.deep.equal([1, 30]); }); it('should properly build a video request with more than one player size', function () { @@ -1879,6 +1842,86 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].rwdd).to.be.undefined; }); + + it('should properly build a request when FLEDGE is enabled', function () { + const bidderRequest = { + fledgeEnabled: true, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext.ae).to.equal(1); + }); + + it('should properly build a request when FLEDGE is disabled', function () { + const bidderRequest = { + fledgeEnabled: false, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext).to.not.have.property('ae'); + }); + + it('should properly transmit device.ext.cdep if available', function () { + const bidderRequest = { + ortb2: { + device: { + ext: { + cdep: 'cookieDeprecationLabel' + } + } + } + }; + const bidRequests = []; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.device.ext.cdep).to.equal('cookieDeprecationLabel'); + }); }); describe('interpretResponse', function () { @@ -2410,6 +2453,144 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].height).to.equal(90); }); + it('should properly parse a bid response with FLEDGE auction configs', function () { + const response = { + body: { + ext: { + igbid: [{ + impid: 'test-bidId', + igbuyer: [{ + origin: 'https://first-buyer-domain.com', + buyerdata: { + foo: 'bar', + }, + }, { + origin: 'https://second-buyer-domain.com', + buyerdata: { + foo: 'baz', + }, + }] + }, { + impid: 'test-bidId-2', + igbuyer: [{ + origin: 'https://first-buyer-domain.com', + buyerdata: { + foo: 'bar', + }, + }, { + origin: 'https://second-buyer-domain.com', + buyerdata: { + foo: 'baz', + }, + }] + }], + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + }, + sellerSignalsPerImp: { + 'test-bidId': { + foo2: 'bar2', + } + }, + }, + }, + }; + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'test-bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + { + bidId: 'test-bidId-2', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const interpretedResponse = spec.interpretResponse(response, request); + expect(interpretedResponse).to.have.property('bids'); + expect(interpretedResponse).to.have.property('fledgeAuctionConfigs'); + expect(interpretedResponse.bids).to.have.lengthOf(0); + expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(2); + expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ + bidId: 'test-bidId', + 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', + }, + }, + }); + 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', + }, + }, + }); + }); + [{ hasBidResponseLevelPafData: true, hasBidResponseBidLevelPafData: true, diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aaf63873d93..975271738e5 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -52,17 +52,21 @@ describe('CriteoId module', function () { }); const storageTestCases = [ - { cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, - { cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, - { cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, - { cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: undefined, localStorage: 'bidId2', expected: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId2' }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: undefined, expected: undefined }, ] - storageTestCases.forEach(testCase => it('getId() should return the bidId when it exists in local storages', function () { + storageTestCases.forEach(testCase => it('getId() should return the user id depending on the storage type enabled and the data available', function () { getCookieStub.withArgs('cto_bidid').returns(testCase.cookie); getLocalStorageStub.withArgs('cto_bidid').returns(testCase.localStorage); - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(testCase.submoduleConfig); expect(result.id).to.be.deep.equal(testCase.expected ? { criteoId: testCase.expected } : undefined); expect(result.callback).to.be.a('function'); })) @@ -95,22 +99,24 @@ describe('CriteoId module', function () { }); const responses = [ - { bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, - { bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, shouldWriteCookie: true, shouldWriteLocalStorage: false, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, shouldWriteCookie: false, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, ] responses.forEach(response => describe('test user sync response behavior', function () { const expirationTs = new Date(nowTimestamp + cookiesMaxAge).toString(); it('should save bidId if it exists', function () { - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(response.submoduleConfig); result.callback((id) => { expect(id).to.be.deep.equal(response.bidId ? { criteoId: response.bidId } : undefined); }); @@ -127,16 +133,35 @@ describe('CriteoId module', function () { expect(setCookieStub.calledWith('cto_bundle')).to.be.false; expect(setLocalStorageStub.calledWith('cto_bundle')).to.be.false; } else if (response.bundle) { - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.false; + } + + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.false; + } expect(triggerPixelStub.called).to.be.false; } if (response.bidId) { - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.false; + } + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.false; + } } else { expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.com')).to.be.true; expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.testdev.com')).to.be.true; diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index f7c2580f3f3..fa44b7daa7a 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -10,11 +10,12 @@ import { addBidResponseHook, currencySupportEnabled, currencyRates, - ready + responseReady } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; import CONSTANTS from '../../../src/constants.json'; import {server} from '../../mocks/xhr.js'; +import * as events from 'src/events.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -32,7 +33,6 @@ describe('currency', function () { beforeEach(function () { fakeCurrencyFileServer = server; - ready.reset(); }); afterEach(function () { @@ -259,6 +259,19 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); }); + it('does not block auctions if rates do not need to be fetched', () => { + sandbox.stub(responseReady, 'resolve'); + setConfig({ + adServerCurrency: 'USD', + rates: { + USD: { + JPY: 100 + } + } + }); + sinon.assert.called(responseReady.resolve); + }) + it('uses rates specified in json when provided and consider boosted bid', function () { setConfig({ adServerCurrency: 'USD', @@ -287,32 +300,56 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('1000.000'); }); - it('uses default rates when currency file fails to load', function () { - setConfig({}); - - setConfig({ - adServerCurrency: 'USD', - defaultRates: { - USD: { - JPY: 100 + describe('when rates fail to load', () => { + let bid, addBidResponse, reject; + beforeEach(() => { + bid = makeBid({cpm: 100, currency: 'JPY', bidder: 'rubicoin'}); + addBidResponse = sinon.spy(); + reject = sinon.spy(); + }) + it('uses default rates if specified', function () { + setConfig({ + adServerCurrency: 'USD', + defaultRates: { + USD: { + JPY: 100 + } } - } - }); - - // default response is 404 - fakeCurrencyFileServer.respond(); + }); - var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; - var innerBid; + // default response is 404 + addBidResponseHook(addBidResponse, 'au', bid); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', sinon.match(innerBid => { + expect(innerBid.cpm).to.equal('1.0000'); + expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); + expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); + return true; + })); + }); - addBidResponseHook(function(adCodeId, bid) { - innerBid = bid; - }, 'elementId', bid); + it('rejects bids if no default rates are specified', () => { + setConfig({ + adServerCurrency: 'USD', + }); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }); - expect(innerBid.cpm).to.equal('1.0000'); - expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); - expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); - }); + it('attempts to load rates again on the next auction', () => { + setConfig({ + adServerCurrency: 'USD', + }); + fakeCurrencyFileServer.respond(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {}); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', bid, reject); + }) + }) }); describe('currency.addBidResponseDecorator bidResponseQueue', function () { @@ -321,29 +358,26 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var bid = { 'cpm': 1, 'currency': 'USD' }; + const bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - var marker = false; - let promiseResolved = false; + let responseAdded = false; + let isReady = false; + responseReady.promise.then(() => { isReady = true }); + addBidResponseHook(Object.assign(function() { - marker = true; - }, { - bail: function (promise) { - promise.then(() => promiseResolved = true); - } + responseAdded = true; }), 'elementId', bid); - expect(marker).to.equal(false); - setTimeout(() => { - expect(promiseResolved).to.be.false; + expect(responseAdded).to.equal(false); + expect(isReady).to.equal(false); fakeCurrencyFileServer.respond(); setTimeout(() => { - expect(marker).to.equal(true); - expect(promiseResolved).to.be.true; + expect(responseAdded).to.equal(true); + expect(isReady).to.equal(true); done(); }); }); @@ -419,6 +453,23 @@ describe('currency', function () { expect(reject.calledOnce).to.be.true; }); + it('should reject bid when rates have not loaded when the auction times out', () => { + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({'adServerCurrency': 'JPY'}); + const bid = makeBid({cpm: 1, currency: 'USD', auctionId: 'aid'}); + const noConversionBid = makeBid({cpm: 1, currency: 'JPY', auctionId: 'aid'}); + const reject = sinon.spy(); + const addBidResponse = sinon.spy(); + addBidResponseHook(addBidResponse, 'au', bid, reject); + addBidResponseHook(addBidResponse, 'au', noConversionBid, reject); + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, {auctionId: 'aid'}); + fakeCurrencyFileServer.respond(); + sinon.assert.calledOnce(addBidResponse); + sinon.assert.calledWith(addBidResponse, 'au', noConversionBid, reject); + sinon.assert.calledOnce(reject); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }) + it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 89485adf28b..4c12e9fa211 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -58,8 +58,18 @@ describe('The DFP video support module', function () { }, options))); const prm = utils.parseQS(url.query); 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); + 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 () { diff --git a/test/spec/modules/dgkeywordRtdProvider_spec.js b/test/spec/modules/dgkeywordRtdProvider_spec.js index 754740b7a64..ff88ea0512f 100644 --- a/test/spec/modules/dgkeywordRtdProvider_spec.js +++ b/test/spec/modules/dgkeywordRtdProvider_spec.js @@ -91,6 +91,22 @@ describe('Digital Garage Keyword Module', function () { expect(dgRtd.getTargetBidderOfDgKeywords(adUnits_no_target)).an('array') .that.is.empty; }); + it('convertKeywordsToString method unit test', function () { + const keywordsTest = [ + { keywords: { param1: 'keywords1' }, result: 'param1=keywords1' }, + { keywords: { param1: 'keywords1', param2: 'keywords2' }, result: 'param1=keywords1,param2=keywords2' }, + { keywords: { p1: 'k1', p2: 'k2', p: 'k' }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: 'k2', p: ['k'] }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k1,p2=k21,p2=k22,p=k' }, + { keywords: { p1: ['k11', 'k12', 'k13'], p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k11,p1=k12,p1=k13,p2=k21,p2=k22,p=k' }, + { keywords: { p1: [], p2: ['', ''], p: [''] }, result: 'p1,p2,p' }, + { keywords: { p1: 1, p2: [1, 'k2'], p: '' }, result: 'p1,p2=k2,p' }, + { keywords: { p1: ['k1', 2, 'k3'], p2: [1, 2], p: 3 }, result: 'p1=k1,p1=k3,p2,p' }, + ]; + for (const test of keywordsTest) { + expect(dgRtd.convertKeywordsToString(test.keywords)).equal(test.result); + } + }) it('should have targets', function () { const adUnits_targets = [ { @@ -242,16 +258,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -275,16 +291,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -318,16 +334,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[1].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[0].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].ortb2Imp).to.be.an('undefined'); if (!IGNORE_SET_ORTB2) { expect(pbjs.getBidderConfig()).to.be.deep.equal({ diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index 078add73046..05216ff126c 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; -import { spec } from 'modules/discoveryBidAdapter.js'; +import { spec, getPmgUID, storage, getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink } from 'modules/discoveryBidAdapter.js'; +import * as utils from 'src/utils.js'; describe('discovery:BidAdapterTests', function () { let bidRequestData = { @@ -11,12 +12,59 @@ describe('discovery:BidAdapterTests', function () { bidder: 'discovery', params: { token: 'd0f4902b616cc5c38cbe0a08676d0ed9', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://discovery.popin.cc', + }, + refererInfo: { + page: 'https://discovery.popin.cc', + stack: [ + 'a.com', + 'b.com' + ] }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + CxSegments: [] + } + } + }, + site: { + domain: 'discovery.popin.cc', + publisher: { + domain: 'discovery.popin.cc' + }, + page: 'https://discovery.popin.cc', + cat: ['IAB-19', 'IAB-20'], }, }, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + keywords: ['travel', 'sport'], + pbadslot: '202309999' + } + } + }, adUnitCode: 'regular_iframe', transactionId: 'd163f9e2-7ecd-4c2c-a3bd-28ceb52a60ee', sizes: [[300, 250]], @@ -51,6 +99,47 @@ describe('discovery:BidAdapterTests', function () { expect(req_data.imp).to.have.lengthOf(1); }); + describe('discovery: buildRequests', function() { + describe('getPmgUID function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(utils, 'generateUUID').returns('new-uuid'); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should generate new UUID and set cookie if not exists', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => null); + const uid = getPmgUID(); + expect(uid).to.equal('new-uuid'); + expect(storage.setCookie.calledOnce).to.be.true; + }); + + it('should return existing UUID from cookie', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => 'existing-uuid'); + const uid = getPmgUID(); + expect(uid).to.equal('existing-uuid'); + expect(storage.setCookie.called).to.be.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('discovery:validate_response_params', function () { let tempAdm = '' tempAdm += '%3Cscr'; @@ -90,4 +179,269 @@ describe('discovery:BidAdapterTests', function () { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); + + describe('discovery: getUserSyncs', function() { + const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html'; + const IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }; + const IFRAME_DISABLED = { + iframeEnabled: false, + pixelEnabled: false, + }; + const GDPR_CONSENT = { + consentString: 'gdprConsentString', + gdprApplies: true + }; + const USP_CONSENT = { + consentString: 'uspConsentString' + } + + let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; + syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; + const expectedIframeSyncs = [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + + it('should return nothing if iframe is disabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.be.undefined; + }); + + it('should do userSyncs if iframe is enabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.deep.equal(expectedIframeSyncs); + }); + }); +}); + +describe('discovery Bid Adapter Tests', function () { + describe('buildRequests', () => { + describe('getPageTitle function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document title if available', function() { + const fakeTopDocument = { + title: 'Top Document Title', + querySelector: () => ({ content: 'Top Document Title test' }) + }; + const fakeTopWindow = { + document: fakeTopDocument + }; + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the content of top og:title meta tag if title is empty', function() { + const ogTitleContent = 'Top OG Title Content'; + const fakeTopWindow = { + document: { + title: '', + querySelector: sandbox.stub().withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }) + } + }; + + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return the document title if no og:title meta tag is present', function() { + document.title = 'Test Page Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + + const result = getPageTitle({ top: undefined }); + expect(result).to.equal('Test Page Title'); + }); + + it('should return the content of og:title meta tag if present', function() { + document.title = ''; + const ogTitleContent = 'Top OG Title Content'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return an empty string if no title or og:title meta tag is found', function() { + document.title = ''; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(''); + }); + + it('should handle exceptions when accessing top.document and fallback to current document', function() { + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const ogTitleContent = 'Current OG Title Content'; + document.title = 'Current Document Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle(fakeWindow); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document description if available', function() { + const descriptionContent = 'Top Document Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[name="description"]').returns({ content: descriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(descriptionContent); + }); + + it('should return the top document og:description if description is not present', function() { + const ogDescriptionContent = 'Top OG Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(ogDescriptionContent); + }); + + it('should return the current document description if top document is not accessible', function() { + const descriptionContent = 'Current Document Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="description"]').returns({ content: descriptionContent }) + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(descriptionContent); + }); + + it('should return the current document og:description if description is not present and top document is not accessible', function() { + const ogDescriptionContent = 'Current OG Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }); + + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(ogDescriptionContent); + }); + }); + + describe('getPageKeywords function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document keywords if available', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + const fakeTopDocument = { + querySelector: sandbox.stub() + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + + const result = getPageKeywords({ top: fakeTopWindow }); + expect(result).to.equal(keywordsContent); + }); + + it('should return the current document keywords if top document is not accessible', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }); + + // æ¨Ąæ‹ŸéĄļåą‚įĒ—åŖčŽŋ问åŧ‚常 + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + + const result = getPageKeywords(fakeWindow); + expect(result).to.equal(keywordsContent); + }); + + it('should return an empty string if no keywords meta tag is found', function() { + sandbox.stub(document, 'querySelector').withArgs('meta[name="keywords"]').returns(null); + + const result = getPageKeywords(); + expect(result).to.equal(''); + }); + }); + describe('getConnectionDownLink function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the downlink value as a string if available', function() { + const downlinkValue = 2.5; + const fakeNavigator = { + connection: { + downlink: downlinkValue + } + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.equal(downlinkValue.toString()); + }); + + it('should return undefined if downlink is not available', function() { + const fakeNavigator = { + connection: {} + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should return undefined if connection is not available', function() { + const fakeNavigator = {}; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should handle cases where navigator is not defined', function() { + const result = getConnectionDownLink({}); + expect(result).to.be.undefined; + }); + }); + }); }); diff --git a/test/spec/modules/docereeAdManagerBidAdapter_spec.js b/test/spec/modules/docereeAdManagerBidAdapter_spec.js new file mode 100644 index 00000000000..26b054f4e29 --- /dev/null +++ b/test/spec/modules/docereeAdManagerBidAdapter_spec.js @@ -0,0 +1,125 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/docereeAdManagerBidAdapter.js'; +import { config } from '../../../src/config.js'; + +describe('docereeadmanager', function () { + config.setConfig({ + docereeadmanager: { + user: { + data: { + email: '', + firstname: '', + lastname: '', + mobile: '', + specialization: '', + organization: '', + hcpid: '', + dob: '', + gender: '', + city: '', + state: '', + country: '', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '', + zipcode: '', + userconsent: '', + }, + }, + }, + }); + let bid = { + bidId: 'testing', + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', + gdpr: '1', + gdprconsent: + 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', + }, + }; + + describe('isBidRequestValid', function () { + it('Should return true if placementId is present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if placementId is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('isGdprConsentPresent', function () { + it('Should return true if gdpr consent is present', function () { + expect(spec.isGdprConsentPresent(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + serverRequest = serverRequest[0]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://dai.doceree.com/drs/quest'); + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: { + cpm: 3.576, + currency: 'USD', + width: 250, + height: 300, + ad: '

I am an ad

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

I am an ad

'); + expect(dataItem.ttl).to.equal(30); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.creativeId).to.equal('div-1'); + expect(dataItem.meta.advertiserDomains).to.be.an('array').that.is.not + .empty; + }); + }); +}); diff --git a/test/spec/modules/docereeBidAdapter_spec.js b/test/spec/modules/docereeBidAdapter_spec.js index dadbb56b0c0..25da8b256fc 100644 --- a/test/spec/modules/docereeBidAdapter_spec.js +++ b/test/spec/modules/docereeBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/docereeBidAdapter.js'; import { config } from '../../../src/config.js'; +import * as utils from 'src/utils.js'; describe('BidlabBidAdapter', function () { config.setConfig({ @@ -102,4 +103,36 @@ describe('BidlabBidAdapter', function () { expect(dataItem.meta.advertiserDomains[0]).to.equal('doceree.com') }); }) + describe('onBidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon([]); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); }); diff --git a/test/spec/modules/dsp_genieeBidAdapter_spec.js b/test/spec/modules/dsp_genieeBidAdapter_spec.js new file mode 100644 index 00000000000..94ec1011fbf --- /dev/null +++ b/test/spec/modules/dsp_genieeBidAdapter_spec.js @@ -0,0 +1,173 @@ +import { expect } from 'chai'; +import { spec } from 'modules/dsp_genieeBidAdapter.js'; +import { config } from 'src/config'; + +describe('Geniee adapter tests', () => { + const validBidderRequest = { + code: 'sample_request', + bids: [{ + bidId: 'bid-id', + bidder: 'dsp_geniee', + params: { + test: 1 + } + }], + gdprConsent: { + gdprApplies: false + }, + uspConsent: '1YNY' + }; + + describe('isBidRequestValid function test', () => { + it('valid', () => { + expect(spec.isBidRequestValid(validBidderRequest.bids[0])).equal(true); + }); + }); + describe('buildRequests function test', () => { + it('auction', () => { + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + const auction_id = request.data.id; + expect(request).deep.equal({ + method: 'POST', + url: 'https://rt.gsspat.jp/prebid_auction', + data: { + at: 1, + id: auction_id, + imp: [ + { + ext: { + test: 1 + }, + id: 'bid-id' + } + ], + test: 1 + }, + }); + }); + it('uncomfortable (gdpr)', () => { + validBidderRequest.gdprConsent.gdprApplies = true; + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + validBidderRequest.gdprConsent.gdprApplies = false; + }); + it('uncomfortable (usp)', () => { + validBidderRequest.uspConsent = '1YYY'; + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + validBidderRequest.uspConsent = '1YNY'; + }); + it('uncomfortable (coppa)', () => { + config.setConfig({ coppa: true }); + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + config.resetConfig(); + }); + it('uncomfortable (currency)', () => { + config.setConfig({ currency: { adServerCurrency: 'TWD' } }); + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + config.resetConfig(); + }); + }); + describe('interpretResponse function test', () => { + it('sample bid', () => { + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + const auction_id = request.data.id; + const adm = "\n"; + const serverResponse = { + body: { + id: auction_id, + cur: 'JPY', + seatbid: [{ + bid: [{ + id: '7b77235d599e06d289e58ddfa9390443e22d7071', + impid: 'bid-id', + price: 0.6666000000000001, + adid: '8405715', + adm: adm, + adomain: ['geniee.co.jp'], + iurl: 'http://img.gsspat.jp/e/068c8e1eafbf0cb6ac1ee95c36152bd2/04f4bd4e6b71f978d343d84ecede3877.png', + cid: '8405715', + crid: '1383823', + cat: ['IAB1'], + w: 300, + h: 250, + mtype: 1 + }] + }] + } + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).deep.equal([{ + ad: adm, + cpm: 0.6666000000000001, + creativeId: '1383823', + creative_id: '1383823', + height: 250, + width: 300, + currency: 'JPY', + mediaType: 'banner', + meta: { + advertiserDomains: ['geniee.co.jp'] + }, + netRevenue: true, + requestId: 'bid-id', + seatBidId: '7b77235d599e06d289e58ddfa9390443e22d7071', + ttl: 300 + }]); + }); + it('no bid', () => { + const serverResponse = {}; + const bids = spec.interpretResponse(serverResponse, validBidderRequest); + expect(bids).deep.equal([]); + }); + }); + describe('getUserSyncs function test', () => { + it('sync enabled', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + const serverResponses = []; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).deep.equal([{ + type: 'image', + url: 'https://rt.gsspat.jp/prebid_cs' + }]); + }); + it('sync disabled (option false)', () => { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: false + }; + const serverResponses = []; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).deep.equal([]); + }); + it('sync disabled (gdpr)', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + const serverResponses = []; + const gdprConsent = { + gdprApplies: true + }; + const syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent); + expect(syncs).deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/dxkultureBidAdapter_spec.js b/test/spec/modules/dxkultureBidAdapter_spec.js new file mode 100644 index 00000000000..a752c81cb6e --- /dev/null +++ b/test/spec/modules/dxkultureBidAdapter_spec.js @@ -0,0 +1,649 @@ +import {expect} from 'chai'; +import {spec, SYNC_URL} from 'modules/dxkultureBidAdapter.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; + +const getBannerRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: 'bidderRequestId', + bids: [ + { + bidder: 'dxkulture', + params: { + placementId: 123456, + publisherId: 'publisherId', + bidfloor: 10, + }, + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + placementCode: 'div-gpt-dummy-placement-code', + mediaTypes: { + banner: { + sizes: [ + [ 300, 250 ], + ] + } + }, + bidId: '2e9f38ea93bb9e', + bidderRequestId: 'bidderRequestId', + } + ], + start: 1487883186070, + auctionStart: 1487883186069, + timeout: 3000 + } +}; + +const getVideoRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + bidderRequestId: '34feaad34lkj2', + bids: [{ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }, { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe2e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }], + auctionStart: 1520001292880, + timeout: 5000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'test.com' + } + }; +}; + +const getBidderResponse = () => { + return { + headers: null, + body: { + id: 'bid-response', + seatbid: [ + { + bid: [ + { + id: '2e9f38ea93bb9e', + impid: '2e9f38ea93bb9e', + price: 0.18, + adm: '', + adid: '144762342', + adomain: [ + 'https://dummydomain.com' + ], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + cat: [], + w: 300, + h: 250, + ext: { + prebid: { + type: 'banner' + }, + bidder: { + appnexus: { + brand_id: 334553, + auction_id: 514667951122925701, + bidder_id: 2, + bid_ad_type: 0 + } + } + } + } + ], + seat: 'dxkulture' + } + ], + ext: { + usersync: { + sovrn: { + status: 'none', + syncs: [ + { + url: 'urlsovrn', + type: 'iframe' + } + ] + }, + appnexus: { + status: 'none', + syncs: [ + { + url: 'urlappnexus', + type: 'pixel' + } + ] + } + }, + responsetimemillis: { + appnexus: 127 + } + } + } + }; +} + +describe('dxkultureBidAdapter', function() { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'dxkulture', + 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + 'bidderRequestId': '34feaad34lkj2', + 'bids': videoBidRequest, + 'auctionStart': 1520001292880, + 'timeout': 3000, + 'start': 1520001292884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 0 + } + }; + }); + + describe('isValidRequest', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('reject requests without params', function () { + bidderRequest.bids[0].params = {}; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + bidderRequest.bids[0].mediaTypes = {} + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + }); + + describe('buildRequests', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bidRequest.url).equal('https://ads.dxkulture.com/pbjs?pid=publisherId&placementId=123456'); + expect(bidRequest.method).equal('POST'); + }); + }); + + context('banner validation', function () { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'dxkulture', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'instream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); + }); + + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + }); + + describe('buildRequests', function () { + let bidderBannerRequest; + let bidRequestsWithMediaTypes; + let mockBidderRequest; + + beforeEach(function() { + bidderBannerRequest = getBannerRequest(); + + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 + } + } + }, { + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + placement: 1, + plcmt: 1, + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); + + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest) + + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bidderBannerRequest.bids[0].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, getBannerRequest(), { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(bidderBannerRequest.bids, req); + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + + expect(data.imp[1].video.w).to.equal(width); + expect(data.imp[1].video.h).to.equal(height); + expect(data.imp[1].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.imp[1]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); + expect(data.imp[1]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should set pubId to e2etest when bid.params.e2etest = true', function () { + bidRequestsWithMediaTypes[0].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest'); + }); + + it('should attach End 2 End test data', function () { + bidRequestsWithMediaTypes[1].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + expect(data.imp[1].bidfloor).to.equal(0); + expect(data.imp[1].video.w).to.equal(640); + expect(data.imp[1].video.h).to.equal(480); + }); + }); + } + }); + + describe('interpretResponse', function() { + context('when mediaType is banner', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBannerRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('have bids', function () { + let bids = spec.interpretResponse(bidderResponse, bidRequest); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', getBidderResponse().body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', getBidderResponse().body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', getBidderResponse().body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', getBidderResponse().body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', getBidderResponse().body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', getBidderResponse().body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains'); + expect(bids[index]).to.have.property('ttl', 300); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + }); + + context('when mediaType is video', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + }); + }); + + describe('getUserSyncs', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should prioritize iframe', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/dynamicAdBoostRtdProvider_spec.js b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js new file mode 100644 index 00000000000..66c24435589 --- /dev/null +++ b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js @@ -0,0 +1,77 @@ +import { subModuleObj as rtdProvider } from 'modules/dynamicAdBoostRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { expect } from 'chai'; + +const configWithParams = { + params: { + keyId: 'dynamic', + adUnits: ['gpt-123'], + threshold: 1 + } +}; + +const configWithoutRequiredParams = { + params: { + keyId: '' + } +}; + +describe('dynamicAdBoost', function() { + let clock; + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(Date.now()); + }); + afterEach(function () { + sandbox.restore(); + }); + describe('init', function() { + describe('initialize without expected params', function() { + it('fails initalize when keyId is not present', function() { + expect(rtdProvider.init(configWithoutRequiredParams)).to.be.false; + }) + }) + + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(rtdProvider.init(configWithParams)).to.be.true; + clock.tick(1000); + expect(loadExternalScript.called).to.be.true; + }) + }); + }); +}) + +describe('markViewed tests', function() { + let sandbox; + const mockObserver = { + unobserve: sinon.spy() + }; + const makeElement = (id) => { + const el = document.createElement('div'); + el.setAttribute('id', id); + return el; + } + const mockEntry = { + target: makeElement('target_id') + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }) + + afterEach(function() { + sandbox.restore() + }) + + it('markViewed returns a function', function() { + expect(rtdProvider.markViewed(mockEntry, mockObserver)).to.be.a('function') + }); + + it('markViewed unobserves', function() { + const func = rtdProvider.markViewed(mockEntry, mockObserver); + func(); + expect(mockObserver.unobserve.calledOnce).to.be.true; + }); +}) diff --git a/test/spec/modules/edge226BidAdapter_spec.js b/test/spec/modules/edge226BidAdapter_spec.js new file mode 100644 index 00000000000..4819d8d4a4e --- /dev/null +++ b/test/spec/modules/edge226BidAdapter_spec.js @@ -0,0 +1,373 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/edge226BidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'edge226' + +describe('Edge226BidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ssp.dauup.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 1597790e652..25e70f12ced 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -238,6 +238,39 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('sovrn', function() { + const userId = { + sovrn: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('sovrn with ext', function() { + const userId = { + sovrn: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('magnite', function() { const userId = { magnite: {'id': 'sample_id'} @@ -304,6 +337,72 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('openx', function () { + const userId = { + openx: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('openx with ext', function () { + const userId = { + openx: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('pubmatic', function() { + const userId = { + pubmatic: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('pubmatic with ext', function() { + const userId = { + pubmatic: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('liveIntentId; getValue call and NO ext', function() { const userId = { lipb: { diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 1a6cfd7afe4..a381d7644a1 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -74,6 +74,53 @@ describe('E-Planning Adapter', function () { }, 'sizes': [[300, 250], [300, 600]], }; + const validBidSpaceNameWithBidFloor = { + bidder: 'eplanning', + 'bidId': BID_ID, + params: { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'sizes': [[300, 250], [300, 600]], + }; + const validBidSpaceOutstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'playerSize': [300, 600], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; + const validBidSpaceInstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; const validBidSpaceOutstream = { 'bidder': 'eplanning', 'bidId': BID_ID, @@ -573,6 +620,26 @@ describe('E-Planning Adapter', function () { expect(e).to.equal(SN + ':300x250,300x600'); }); + it('should return e parameter with space name attribute with value according to the adunit sizes and bidFloor', function () { + const e = spec.buildRequests([validBidSpaceNameWithBidFloor], bidderRequest).data.e; + expect(e).to.equal(SN + ':300x250,300x600|' + validBidSpaceNameWithBidFloor.getFloor().floor); + }); + + it('should return correct e parameter with support vast with one space with size outstream and bidFloor', function () { + const data = spec.buildRequests([validBidSpaceOutstreamWithBidFloor], bidderRequest).data; + expect(data.e).to.equal('video_300x600_0:300x600;1|' + validBidSpaceOutstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(2); + expect(data.vv).to.equal(3); + }); + + it('should return correct e parameter with support vast with one space with size instream with bidFloor', function () { + let bidRequests = [validBidSpaceInstreamWithBidFloor]; + const data = spec.buildRequests(bidRequests, bidderRequest).data; + expect(data.e).to.equal('video_640x480_0:640x480;1|' + validBidSpaceInstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(1); + expect(data.vv).to.equal(3); + }); + it('should return correct e parameter with more than one adunit', function () { const NEW_CODE = ADUNIT_CODE + '2'; const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2'; diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 4f6bacebe6a..98770fa80bc 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -3,9 +3,11 @@ import {config} from 'src/config.js'; import {euidIdSubmodule} from 'modules/euidIdSystem.js'; import 'modules/consentManagement.js'; import 'src/prebid.js'; +import * as utils from 'src/utils.js'; import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -21,21 +23,26 @@ const auctionDelayMs = 10; const makeEuidIdentityContainer = (token) => ({euid: {id: token}}); const useLocalStorage = true; + const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'euid', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}, ...extraSettings}] }, debug }); +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; + const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; +const cstgApiUrl = 'https://prod.euid.eu/v2/token/client-generate'; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); +const makeSuccessResponseBody = (token) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: token } })); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { - let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; - let server; + let suiteSandbox, restoreSubtleToUndefined = false; const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureEuidCstgResponse = (httpStatus, response) => server.respondWith('POST', cstgApiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); before(function() { uninstallGdprEnforcement(); @@ -43,22 +50,28 @@ describe('EUID module', function() { suiteSandbox = sinon.sandbox.create(); if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function() { suiteSandbox.restore(); if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); beforeEach(function() { - server = sinon.createFakeServer(); init(config); setSubmoduleRegistry([euidIdSubmodule]); }); afterEach(function() { - server.restore(); $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); @@ -115,10 +128,23 @@ describe('EUID module', function() { it('When an expired token is provided and the API responds in time, the refreshed token is provided to the auction.', async function() { setGdprApplies(true); const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); - configureEuidResponse(200, makeSuccessResponseBody()); + configureEuidResponse(200, makeSuccessResponseBody(refreshedToken)); config.setConfig(makePrebidConfig({euidToken})); apiHelpers.respondAfterDelay(1, server); const bid = await runAuction(); expectToken(bid, refreshedToken); }); + + if (FEATURES.UID2_CSTG) { + it('Should use client side generated EUID token in the auction.', async function() { + setGdprApplies(true); + const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); + configureEuidCstgResponse(200, makeSuccessResponseBody(clientSideGeneratedToken)); + config.setConfig(makePrebidConfig({ euidToken, ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(1, server); + + const bid = await runAuction(); + expectToken(bid, clientSideGeneratedToken); + }); + } }); diff --git a/test/spec/modules/experianRtdProvider_spec.js b/test/spec/modules/experianRtdProvider_spec.js new file mode 100644 index 00000000000..fd104674d70 --- /dev/null +++ b/test/spec/modules/experianRtdProvider_spec.js @@ -0,0 +1,365 @@ +import { + EXPERIAN_RTID_DATA_KEY, + EXPERIAN_RTID_EXPIRATION_KEY, + EXPERIAN_RTID_STALE_KEY, + SUBMODULE_NAME, + experianRtdObj, + experianRtdSubmodule, EXPERIAN_RTID_NO_TRACK_KEY +} from '../../../modules/experianRtdProvider.js'; +import { getStorageManager } from '../../../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../../../src/activities/modules'; +import { safeJSONParse, timestamp } from '../../../src/utils'; +import {server} from '../../mocks/xhr.js'; + +describe('Experian realtime module', () => { + const sandbox = sinon.createSandbox(); + let requests; + + const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }) + beforeEach(() => { + requests = server.requests; + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null) + }) + afterEach(() => { + sandbox.restore(); + }) + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'appnexus' } + ] + }] + }; + describe('init', () => { + it('succeeds when params have accountId', () => { + const initResult = experianRtdSubmodule.init({ params: { accountId: 'ZylatYg' } }) + expect(initResult).to.be.true; + }) + + it('fails when params don\'t have accountId', () => { + const initResult = experianRtdSubmodule.init({ }) + expect(initResult).to.be.false; + }) + }) + + describe('getBidRequestData', () => { + describe('when local storage has data, isn\'t no track, isn\'t stale and isn\'t expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t request data envelope, and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + + describe('when local storage has data but it is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('it requests data envelope and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + }) + }) + describe('when local storage has data but it is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no data envelope', () => { + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no track and is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and isn\'t expired or stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t alter bids and doesn\'t request data envelope', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + }) + + describe('alterBids', () => { + describe('data envelope has every bidder from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({pubmatic: { + experianRtidKey: 'pubmatic-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + describe('data envelope is missing bidders from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({ + sovrn: { + experianRtidKey: 'sovrn-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + }) + + describe('requestDataEnvelope', () => { + it('sends request to experian rtd and stores response', () => { + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.requestDataEnvelope(moduleConfig, { gdpr: { gdprApplies: 0, consentString: 'wow' }, uspConsent: '1YYY' }) + requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + '{"staleAt":"2023-06-01T00:00:00","expiresAt":"2023-06-03T00:00:00","status":"ok","data":[{"bidder":"pubmatic","data":{"key":"pubmatic-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}},{"bidder":"sovrn","data":{"key":"sovrn-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}}]}' + ) + + expect(requests[0].url).to.equal('https://rtid.tapad.com/acc/ZylatYg/ids?gdpr=0&gdpr_consent=wow&us_privacy=1YYY') + expect(safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null))).to.deep.equal([{bidder: 'pubmatic', data: {key: 'pubmatic-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}, {bidder: 'sovrn', data: {key: 'sovrn-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}]) + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY)).to.equal('2023-06-01T00:00:00') + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY)).to.equal('2023-06-03T00:00:00') + }) + }) + + describe('extractConsentQueryString', () => { + describe('when userConsent is empty', () => { + it('returns undefined', () => { + expect(experianRtdObj.extractConsentQueryString({})).to.be.undefined + }) + }) + + describe('when userConsent exists', () => { + it('builds query string', () => { + expect( + experianRtdObj.extractConsentQueryString({}, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' }) + ).to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY') + }) + }) + + describe('when config.ids exists', () => { + it('builds query string', () => { + expect(experianRtdObj.extractConsentQueryString({ params: { accountId: 'ZylatYg', ids: { maid: ['424', '2982'], hem: 'my-hem' } } }, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' })) + .to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY&id.maid=424&id.maid=2982&id.hem=my-hem') + }) + }) + }) +}) diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js new file mode 100644 index 00000000000..60a8e196ae0 --- /dev/null +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -0,0 +1,419 @@ +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 * 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'; + +describe('fledgeForGpt module', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + 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); + }); + + 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'); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'b1', + auctionConfig: cf1, + }] + }); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'b2', + auctionConfig: cf2, + }] + }); + }); + + 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'}); + 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'}); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: { + ...fledgeAuctionConfig, + auctionSignals: { + prebid: { + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + } + } + }, + }] + }) + }) + + 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: 'aid'}; + setup(payload, values); + }); + + 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) + })) + }) + }); + }); + }) + }) + }); + }); + }); + + describe('fledgeEnabled', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { + navigator[p] = sinon.stub(); + }); + hook.ready(); + }); + + after(function () { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }); + + afterEach(function () { + config.resetConfig(); + }); + + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + '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) + ); + } + + 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/fledge_spec.js b/test/spec/modules/fledge_spec.js deleted file mode 100644 index 7a670fa2381..00000000000 --- a/test/spec/modules/fledge_spec.js +++ /dev/null @@ -1,282 +0,0 @@ -import { - expect -} from 'chai'; -import * as fledge from 'modules/fledgeForGpt.js'; -import {config} from '../../../src/config.js'; -import adapterManager from '../../../src/adapterManager.js'; -import * as utils from '../../../src/utils.js'; -import {hook} from '../../../src/hook.js'; -import 'modules/appnexusBidAdapter.js'; -import 'modules/rubiconBidAdapter.js'; -import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; - -const CODE = 'sampleBidder'; -const AD_UNIT_CODE = 'mock/placement'; - -describe('fledgeForGpt module', function() { - let nextFnSpy; - before(() => { - fledge.init({enabled: true}) - }); - - const bidRequest = { - adUnitCode: AD_UNIT_CODE, - bids: [{ - bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: AD_UNIT_CODE, - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - } - - describe('addComponentAuctionHook', function() { - beforeEach(function() { - nextFnSpy = sinon.spy(); - }); - - it('should call next() when a proper adUnitCode and fledgeAuctionConfig are provided', function() { - fledge.addComponentAuctionHook(nextFnSpy, bidRequest.adUnitCode, fledgeAuctionConfig); - expect(nextFnSpy.called).to.be.true; - }); - }); -}); - -describe('fledgeEnabled', function () { - const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])) - - before(function () { - // navigator.runAdAuction & co may not exist, so we can't stub it normally with - // sinon.stub(navigator, 'runAdAuction') or something - Object.keys(navProps).forEach(p => { navigator[p] = sinon.stub() }); - hook.ready(); - }); - - after(function() { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); - }) - - afterEach(function () { - config.resetConfig(); - }); - - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - describe('with setBidderConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - defaultForSlots: 1, - } - }); - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - expect(bidRequests[1].defaultForSlots).to.be.undefined; - }); - }); - - describe('with setConfig()', () => { - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - bidders: ['appnexus'], - defaultForSlots: 1, - } - }); - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - expect(bidRequests[1].defaultForSlots).to.be.undefined; - }); - - it('should set fledgeEnabled correctly for all bidders', function () { - config.setConfig({ - bidderSequence: 'fixed', - fledgeForGpt: { - enabled: true, - defaultForSlots: 1, - } - }); - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - expect(bidRequests[0].defaultForSlots).to.equal(1); - }); - }); -}); - -describe('ortb processors for fledge', () => { - describe('when defaultForSlots is set', () => { - it('imp.ext.ae should be set if fledge is enabled', () => { - const imp = {}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); - expect(imp.ext.ae).to.equal(1); - }); - it('imp.ext.ae should be left intact if set on adunit and fledge is enabled', () => { - const imp = {ext: {ae: 2}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true, defaultForSlots: 1}}); - expect(imp.ext.ae).to.equal(2); - }); - }); - describe('when defaultForSlots is not defined', () => { - 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/flippBidAdapter_spec.js b/test/spec/modules/flippBidAdapter_spec.js new file mode 100644 index 00000000000..518052ad91e --- /dev/null +++ b/test/spec/modules/flippBidAdapter_spec.js @@ -0,0 +1,170 @@ +import {expect} from 'chai'; +import {spec} from 'modules/flippBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +describe('flippAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'flipp', + params: { + publisherNameIdentifier: 'random', + siteId: 1234, + zoneIds: [1, 2, 3, 4], + } + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidBid = Object.assign({}, bid); + invalidBid.params = { siteId: 1234 } + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + bidder: 'flipp', + params: { + siteId: 1234, + }, + adUnitCode: '/10000/unit_code', + sizes: [[300, 600]], + mediaTypes: {banner: {sizes: [[300, 600]]}}, + bidId: '237f4d1a293f99', + bidderRequestId: '1a857fa34c1c96', + auctionId: 'a297d1aa-7900-4ce4-a0aa-caa8d46c4af7', + transactionId: '00b2896c-2731-4f01-83e4-7a3ad5da13b6', + }]; + const bidderRequest = { + refererInfo: { + referer: 'http://example.com' + } + }; + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to ENDPOINT with query parameter', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function() { + it('should get correct bid response', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [{ + 'bidCpm': 1, + 'adId': 262838368, + 'height': 600, + 'width': 300, + 'storefront': { 'flyer_id': 5435567 }, + 'prebid': { + 'requestId': '237f4d1a293f99', + 'cpm': 1.11, + 'creative': 'Returned from server', + } + }] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const expectedResponse = [ + { + bidderCode: 'flipp', + requestId: '237f4d1a293f99', + currency: 'USD', + cpm: 1.11, + netRevenue: true, + width: 300, + height: 600, + creativeId: 262838368, + ttl: 30, + ad: 'Returned from server', + } + ]; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectedResponse); + }); + + it('should get empty bid response when no ad is returned', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(0); + expect(result).to.deep.have.same.members([]); + }) + + it('should get empty response when bid server returns 204', function() { + expect(spec.interpretResponse({})).to.be.empty; + }); + }); +}); diff --git a/test/spec/modules/freepassIdSystem_spec.js b/test/spec/modules/freepassIdSystem_spec.js index f7407f5eb94..0a9fa956cd4 100644 --- a/test/spec/modules/freepassIdSystem_spec.js +++ b/test/spec/modules/freepassIdSystem_spec.js @@ -1,4 +1,4 @@ -import {freepassIdSubmodule} from 'modules/freepassIdSystem'; +import { freepassIdSubmodule, storage, FREEPASS_COOKIE_KEY } from 'modules/freepassIdSystem'; import sinon from 'sinon'; import * as utils from '../../../src/utils'; @@ -6,15 +6,16 @@ let expect = require('chai').expect; describe('FreePass ID System', function () { const UUID = '15fde1dc-1861-4894-afdf-b757272f3568'; + let getCookieStub; before(function () { - sinon.stub(utils, 'generateUUID').returns(UUID); sinon.stub(utils, 'logMessage'); + getCookieStub = sinon.stub(storage, 'getCookie'); }); after(function () { - utils.generateUUID.restore(); utils.logMessage.restore(); + getCookieStub.restore(); }); describe('freepassIdSubmodule', function () { @@ -39,71 +40,19 @@ describe('FreePass ID System', function () { }; it('should return an IdObject with a UUID', function () { + getCookieStub.withArgs(FREEPASS_COOKIE_KEY).returns(UUID); const objectId = freepassIdSubmodule.getId(config, undefined); expect(objectId).to.be.an('object'); expect(objectId.id).to.be.an('object'); expect(objectId.id.userId).to.equal(UUID); }); - it('should include userIp in IdObject', function () { + it('should return an IdObject without UUID when absent in cookie', function () { + getCookieStub.withArgs(FREEPASS_COOKIE_KEY).returns(null); const objectId = freepassIdSubmodule.getId(config, undefined); expect(objectId).to.be.an('object'); expect(objectId.id).to.be.an('object'); - expect(objectId.id.userIp).to.equal('127.0.0.1'); - }); - it('should skip userIp in IdObject if not available', function () { - const localConfig = Object.assign({}, config); - delete localConfig.params.freepassData.userIp; - const objectId = freepassIdSubmodule.getId(localConfig, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.userIp).to.be.undefined; - }); - it('should skip userIp in IdObject if freepassData is not available', function () { - const localConfig = JSON.parse(JSON.stringify(config)); - delete localConfig.params.freepassData; - const objectId = freepassIdSubmodule.getId(localConfig, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.userIp).to.be.undefined; - }); - it('should skip userIp in IdObject if params is not available', function () { - const localConfig = JSON.parse(JSON.stringify(config)); - delete localConfig.params; - const objectId = freepassIdSubmodule.getId(localConfig, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.userIp).to.be.undefined; - }); - it('should include commonId in IdObject', function () { - const objectId = freepassIdSubmodule.getId(config, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.commonId).to.equal('commonId'); - }); - it('should skip commonId in IdObject if not available', function () { - const localConfig = Object.assign({}, config); - delete localConfig.params.freepassData.commonId; - const objectId = freepassIdSubmodule.getId(localConfig, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.commonId).to.be.undefined; - }); - it('should skip commonId in IdObject if freepassData is not available', function () { - const localConfig = JSON.parse(JSON.stringify(config)); - delete localConfig.params.freepassData; - const objectId = freepassIdSubmodule.getId(localConfig, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.commonId).to.be.undefined; - }); - it('should skip commonId in IdObject if params is not available', function () { - const localConfig = JSON.parse(JSON.stringify(config)); - delete localConfig.params; - const objectId = freepassIdSubmodule.getId(localConfig, undefined); - expect(objectId).to.be.an('object'); - expect(objectId.id).to.be.an('object'); - expect(objectId.id.commonId).to.be.undefined; + expect(objectId.id.userId).to.be.undefined; }); }); @@ -142,12 +91,15 @@ describe('FreePass ID System', function () { }; it('should return cachedIdObject if there are no changes', function () { + getCookieStub.withArgs(FREEPASS_COOKIE_KEY).returns(UUID); const idObject = freepassIdSubmodule.getId(config, undefined); const cachedIdObject = Object.assign({}, idObject.id); const extendedIdObject = freepassIdSubmodule.extendId(config, undefined, cachedIdObject); expect(extendedIdObject).to.be.an('object'); expect(extendedIdObject.id).to.be.an('object'); - expect(extendedIdObject.id).to.equal(cachedIdObject); + expect(extendedIdObject.id.userId).to.equal(UUID); + expect(extendedIdObject.id.userIp).to.equal(config.params.freepassData.userIp); + expect(extendedIdObject.id.commonId).to.equal(config.params.freepassData.commonId); }); it('should return cachedIdObject if there are no new data', function () { @@ -182,5 +134,20 @@ describe('FreePass ID System', function () { expect(extendedIdObject.id).to.be.an('object'); expect(extendedIdObject.id.userIp).to.equal('192.168.1.1'); }); + + it('should return new userId when changed from cache', function () { + getCookieStub.withArgs(FREEPASS_COOKIE_KEY).returns(UUID); + const idObject = freepassIdSubmodule.getId(config, undefined); + const cachedIdObject = Object.assign({}, idObject.id); + const localConfig = JSON.parse(JSON.stringify(config)); + localConfig.params.freepassData.userIp = '192.168.1.1'; + + getCookieStub.withArgs(FREEPASS_COOKIE_KEY).returns('NEW_UUID'); + const extendedIdObject = freepassIdSubmodule.extendId(localConfig, undefined, cachedIdObject); + expect(extendedIdObject).to.be.an('object'); + expect(extendedIdObject.id).to.be.an('object'); + expect(extendedIdObject.id.userIp).to.equal('192.168.1.1'); + expect(extendedIdObject.id.userId).to.equal('NEW_UUID'); + }); }); }); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index c42c5e2528d..90ebe0b80ee 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/freewheel-sspBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { createEidsArray } from 'modules/userId/eids.js'; +import { config } from 'src/config.js'; const ENDPOINT = '//ads.stickyadstv.com/www/delivery/swfIndex.php'; const PREBID_VERSION = '$prebid.version$'; @@ -203,6 +204,14 @@ describe('freewheelSSP BidAdapter Test', () => { let bidderRequest = { 'gdprConsent': { 'consentString': gdprConsentString + }, + 'ortb2': { + 'site': { + 'content': { + 'test': 'news', + 'test2': 'param' + } + } } }; @@ -216,6 +225,7 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + expect(payload._fw_prebid_content).to.deep.equal('{\"test\":\"news\",\"test2\":\"param\"}'); let gdprConsent = { 'gdprApplies': true, diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 1d0eee923b9..295b14dd796 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,13 +1,12 @@ import { accessDeviceRule, - deviceAccessHook, - enforcementRules, enrichEidsRule, fetchBidsRule, + transmitEidsRule, + transmitPreciseGeoRule, getGvlid, getGvlidFromAnalyticsAdapter, - purpose1Rule, - purpose2Rule, + ACTIVE_RULES, reportAnalyticsRule, setEnforcementConfig, STRICT_STORAGE_ENFORCEMENT, @@ -27,7 +26,7 @@ import * as events from 'src/events.js'; import 'modules/appnexusBidAdapter.js'; // some tests expect this to be in the adapter registry import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; -import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../../../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID, FIRST_PARTY_GVLID} from '../../../src/consentHandler.js'; import {validateStorageEnforcement} from '../../../src/storageManager.js'; import {activityParams} from '../../../src/activities/activityParams.js'; @@ -474,6 +473,261 @@ describe('gdpr enforcement', function () { }); }); + describe('transmitEidsRule', () => { + const GVL_ID = 123; + const BIDDER = 'mockBidder'; + let cd; + const RULES_2_10 = { + basicAds: 2, + personalizedAds: 4, + measurement: 7, + } + + beforeEach(() => { + cd = setupConsentData(); + cd.vendorData = { + vendor: { + consents: {}, + legitimateInterests: {}, + }, + purpose: { + consents: {}, + legitimateInterests: {} + } + }; + Object.assign(gvlids, { + [BIDDER]: GVL_ID + }); + }); + + function setVendorConsent(type = 'consents') { + cd.vendorData.vendor[type][GVL_ID] = true; + } + + function runRule() { + return transmitEidsRule(activityParams(MODULE_TYPE_BIDDER, BIDDER)); + } + + describe('default behavior', () => { + const CS_PURPOSES = [3, 4, 5, 6, 7, 8, 9, 10]; + const LI_PURPOSES = [2]; + const CONSENT_TYPES = ['consents', 'legitimateInterests']; + + describe('should deny if', () => { + describe('config is default', () => { + beforeEach(() => { + setEnforcementConfig({}); + }); + + it('no consent is given, of any type or for any vendor', () => { + expectAllow(false, runRule()); + }); + + CONSENT_TYPES.forEach(type => { + it(`vendor ${type} is given, but no purpose has consent`, () => { + setVendorConsent(type); + expectAllow(false, runRule()); + }); + + it(`${type} is given for purpose other than 2-10`, () => { + setVendorConsent(type); + cd.vendorData.purpose[type][1] = true; + expectAllow(false, runRule()); + }); + + LI_PURPOSES.forEach(purpose => { + it(`purpose ${purpose} has ${type}, but vendor does not`, () => { + cd.vendorData.purpose[type][purpose] = true; + expectAllow(false, runRule()); + }); + }); + }); + }); + + describe(`no consent is given`, () => { + [ + { + enforcePurpose: false, + }, + { + enforceVendor: false, + }, + { + enforcePurpose: false, + enforceVendor: false, + } + ].forEach(t => { + it(`config has ${JSON.stringify(t)} for each of ${Object.keys(RULES_2_10).join(', ')}`, () => { + setEnforcementConfig({ + gdpr: { + rules: Object.keys(RULES_2_10).map(rule => Object.assign({ + purpose: rule, + vendorExceptions: [], + enforcePurpose: true, + enforceVendor: true + }, t)) + } + }); + expectAllow(false, runRule()); + }); + }); + }); + }); + + describe('should allow if', () => { + describe('config is default', () => { + beforeEach(() => { + setEnforcementConfig({}); + }); + LI_PURPOSES.forEach(purpose => { + it(`purpose ${purpose} has LI, vendor has LI`, () => { + setVendorConsent('legitimateInterests'); + cd.vendorData.purpose.legitimateInterests[purpose] = true; + expectAllow(true, runRule()); + }); + }); + + LI_PURPOSES.concat(CS_PURPOSES).forEach(purpose => { + it(`purpose ${purpose} has consent, vendor has consent`, () => { + setVendorConsent(); + cd.vendorData.purpose.consents[purpose] = true; + expectAllow(true, runRule()); + }); + }); + }); + + Object.keys(RULES_2_10).forEach(rule => { + it(`no consent given, but '${rule}' config has a vendor exception`, () => { + setEnforcementConfig({ + gdpr: { + rules: [ + { + purpose: rule, + enforceVendor: false, + enforcePurpose: false, + vendorExceptions: [BIDDER] + } + ] + } + }); + expectAllow(true, runRule()); + }); + + it(`vendor consent is missing, but '${rule}' config has a softVendorException`, () => { + setEnforcementConfig({ + gdpr: { + rules: [ + { + purpose: rule, + enforceVendor: false, + enforcePurpose: false, + softVendorExceptions: [BIDDER] + } + ] + } + }); + cd.vendorData.purpose.consents[RULES_2_10[rule]] = true; + expectAllow(true, runRule()); + }) + }); + }); + }); + + describe('with eidsRequireP4consent', () => { + function setupPAdsRule(cfg = {}) { + setEnforcementConfig({ + gdpr: { + rules: [ + Object.assign({ + purpose: 'personalizedAds', + eidsRequireP4Consent: true, + enforcePurpose: true, + enforceVendor: true, + }, cfg) + ] + } + }) + } + describe('allows when', () => { + Object.entries({ + 'purpose 4 consent is given'() { + setupPAdsRule(); + setVendorConsent(); + cd.vendorData.purpose.consents[4] = true + }, + 'enforcePurpose is false, with vendor consent given'() { + setupPAdsRule({enforcePurpose: false}); + setVendorConsent(); + }, + 'enforceVendor is false, with purpose consent given'() { + setupPAdsRule({enforceVendor: false}); + cd.vendorData.purpose.consents[4] = true; + }, + 'vendor is excepted'() { + setupPAdsRule({vendorExceptions: [BIDDER]}); + }, + 'vendor is softly excepted, with purpose consent given'() { + setupPAdsRule({softVendorExceptions: [BIDDER]}); + cd.vendorData.purpose.consents[4] = true; + } + }).forEach(([t, setup]) => { + it(t, () => { + setup(); + expectAllow(true, runRule()); + }); + }); + }); + describe('denies when', () => { + Object.entries({ + 'purpose 4 consent is not given'() { + setupPAdsRule(); + setVendorConsent(); + }, + 'vendor consent is not given'() { + setupPAdsRule(); + cd.vendorData.purpose.consents[4] = true + }, + }).forEach(([t, setup]) => { + it(t, () => { + setup(); + expectAllow(false, runRule()); + }) + }) + }) + }) + }); + + describe('transmitPreciseGeoRule', () => { + const BIDDER = 'mockBidder'; + let cd; + + function runRule() { + return transmitPreciseGeoRule(activityParams(MODULE_TYPE_BIDDER, BIDDER)) + } + + beforeEach(() => { + cd = setupConsentData(); + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'transmitPreciseGeo', + enforcePurpose: true, + enforceVendor: false + }] + } + }) + }); + + it('should allow when special feature 1 consent is given', () => { + cd.vendorData.specialFeatureOptins[1] = true; + expectAllow(true, runRule()); + }) + it('should deny when configured, but consent is missing', () => { + cd.vendorData.specialFeatureOptins[1] = false; + expectAllow(false, runRule()); + }); + }); + describe('validateRules', function () { const createGdprRule = (purposeName = 'storage', enforcePurpose = true, enforceVendor = true, vendorExceptions = [], softVendorExceptions = []) => ({ purpose: purposeName, @@ -535,6 +789,21 @@ describe('gdpr enforcement', function () { }) }) + it('if validateRules is passed FIRST_PARTY_GVLID, it will use publisher.consents', () => { + const rule = createGdprRule(); + const consentData = { + 'vendorData': { + 'publisher': { + 'consents': { + '1': true + } + }, + }, + }; + const result = validateRules(rule, consentData, 'cdep', FIRST_PARTY_GVLID); + expect(result).to.equal(true); + }); + describe('validateRules', function () { Object.entries({ '1 (which does not consider LI)': [1, 'storage', false], @@ -631,7 +900,8 @@ describe('gdpr enforcement', function () { }); expect(logWarnSpy.calledOnce).to.equal(true); - expect(enforcementRules).to.deep.equal(DEFAULT_RULES); + expect(ACTIVE_RULES.purpose[1]).to.deep.equal(DEFAULT_RULES[0]); + expect(ACTIVE_RULES.purpose[2]).to.deep.equal(DEFAULT_RULES[1]); }); it('should enforce TCF2 Purpose 2 also if only Purpose 1 is defined in "rules"', function () { @@ -647,8 +917,8 @@ describe('gdpr enforcement', function () { } }); - expect(purpose1Rule).to.deep.equal(purpose1RuleDefinedInConfig); - expect(purpose2Rule).to.deep.equal(DEFAULT_RULES[1]); + expect(ACTIVE_RULES.purpose[1]).to.deep.equal(purpose1RuleDefinedInConfig); + expect(ACTIVE_RULES.purpose[2]).to.deep.equal(DEFAULT_RULES[1]); }); it('should enforce TCF2 Purpose 1 also if only Purpose 2 is defined in "rules"', function () { @@ -664,8 +934,8 @@ describe('gdpr enforcement', function () { } }); - expect(purpose1Rule).to.deep.equal(DEFAULT_RULES[0]); - expect(purpose2Rule).to.deep.equal(purpose2RuleDefinedInConfig); + expect(ACTIVE_RULES.purpose[1]).to.deep.equal(DEFAULT_RULES[0]); + expect(ACTIVE_RULES.purpose[2]).to.deep.equal(purpose2RuleDefinedInConfig); }); it('should use the "rules" defined in config if a definition found', function() { @@ -679,8 +949,8 @@ describe('gdpr enforcement', function () { enforceVendor: false }] setEnforcementConfig({gdpr: { rules }}); - - expect(enforcementRules).to.deep.equal(rules); + expect(ACTIVE_RULES.purpose[1]).to.deep.equal(rules[0]); + expect(ACTIVE_RULES.purpose[2]).to.deep.equal(rules[1]); }); }); diff --git a/test/spec/modules/genericAnalyticsAdapter_spec.js b/test/spec/modules/genericAnalyticsAdapter_spec.js index a5a6074c425..79874f5d756 100644 --- a/test/spec/modules/genericAnalyticsAdapter_spec.js +++ b/test/spec/modules/genericAnalyticsAdapter_spec.js @@ -75,7 +75,7 @@ describe('Generic analytics', () => { options: { url: 'mock', events: { - bidResponse: null + mockEvent: null } } }); diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js index 96da2e3dbd7..211a3efa3c6 100644 --- a/test/spec/modules/geoedgeRtdProvider_spec.js +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -1,17 +1,21 @@ import * as utils from '../../../src/utils.js'; import {loadExternalScript} from '../../../src/adloader.js'; -import { +import * as geoedgeRtdModule from '../../../modules/geoedgeRtdProvider.js'; +import {server} from '../../../test/mocks/xhr.js'; +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; + +let { geoedgeSubmodule, getClientUrl, getInPageUrl, htmlPlaceholder, setWrapper, - wrapper, - WRAPPER_URL -} from '../../../modules/geoedgeRtdProvider.js'; -import {server} from '../../../test/mocks/xhr.js'; -import * as events from '../../../src/events.js'; -import CONSTANTS from '../../../src/constants.json'; + getMacros, + WRAPPER_URL, + preloadClient, + markAsLoaded +} = geoedgeRtdModule; let key = '123123123'; function makeConfig(gpt) { @@ -64,13 +68,11 @@ describe('Geoedge RTD module', function () { }); }); describe('init', function () { - let insertElementStub; - before(function () { - insertElementStub = sinon.stub(utils, 'insertElement'); + sinon.spy(geoedgeRtdModule, 'preloadClient'); }); after(function () { - utils.insertElement.restore(); + geoedgeRtdModule.preloadClient.restore(); }); it('should return false when missing params or key', function () { let missingParams = geoedgeSubmodule.init({}); @@ -86,14 +88,13 @@ describe('Geoedge RTD module', function () { let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; expect(isWrapperRequest).to.equal(true); }); - it('should preload the client', function () { - let isLinkPreloadAsScript = arg => arg.tagName === 'LINK' && arg.rel === 'preload' && arg.as === 'script' && arg.href === getClientUrl(key); - expect(insertElementStub.calledWith(sinon.match(isLinkPreloadAsScript))).to.equal(true); + it('should call preloadClient', function () { + expect(preloadClient.called); }); it('should emit billable events with applicable winning bids', function (done) { let counter = 0; events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, function (event) { - if (event.vendor === 'geoedge' && event.type === 'impression') { + if (event.vendor === geoedgeSubmodule.name && event.type === 'impression') { counter += 1; } expect(counter).to.equal(1); @@ -103,7 +104,7 @@ describe('Geoedge RTD module', function () { }); it('should load the in page code when gpt params is true', function () { geoedgeSubmodule.init(makeConfig(true)); - let isInPageUrl = arg => arg == getInPageUrl(key); + let isInPageUrl = arg => arg === getInPageUrl(key); expect(loadExternalScript.calledWith(sinon.match(isInPageUrl))).to.equal(true); }); it('should set the window.grumi config object when gpt params is true', function () { @@ -111,10 +112,36 @@ describe('Geoedge RTD module', function () { expect(hasGrumiObj && window.grumi.key === key && window.grumi.fromPrebid).to.equal(true); }); }); + describe('preloadClient', function () { + let iframe; + preloadClient(key); + let loadExternalScriptCall = loadExternalScript.getCall(0); + it('should create an invisible iframe and insert it to the DOM', function () { + iframe = document.getElementById('grumiFrame'); + expect(iframe && iframe.style.display === 'none'); + }); + it('should assign params object to the iframe\'s window', function () { + let grumi = iframe.contentWindow.grumi; + expect(grumi.key).to.equal(key); + }); + it('should preload the client into the iframe', function () { + let isClientUrl = arg => arg === getClientUrl(key); + expect(loadExternalScriptCall.calledWithMatch(isClientUrl)).to.equal(true); + }); + }); describe('setWrapper', function () { it('should set the wrapper', function () { setWrapper(mockWrapper); - expect(wrapper).to.equal(mockWrapper); + expect(geoedgeRtdModule.wrapper).to.equal(mockWrapper); + }); + }); + describe('getMacros', function () { + it('return a dictionary of macros replaced with values from bid object', function () { + let bid = mockBid('testBidder'); + let dict = getMacros(bid, key); + let hasCpm = dict['%_hbCpm!'] === bid.cpm; + let hasCurrency = dict['%_hbCurrency!'] === bid.currency; + expect(hasCpm && hasCurrency); }); }); describe('onBidResponseEvent', function () { diff --git a/test/spec/modules/goldfishAdsRtdProvider_spec.js b/test/spec/modules/goldfishAdsRtdProvider_spec.js new file mode 100755 index 00000000000..39a1e0c9b33 --- /dev/null +++ b/test/spec/modules/goldfishAdsRtdProvider_spec.js @@ -0,0 +1,163 @@ +import { + goldfishAdsSubModule, + manageCallbackResponse, +} from 'modules/goldfishAdsRtdProvider.js'; +import { getStorageManager } from '../../../src/storageManager.js'; +import { expect } from 'chai'; +import { server } from 'test/mocks/xhr.js'; +import { config as _config } from 'src/config.js'; +import { DATA_STORAGE_KEY, MODULE_NAME, MODULE_TYPE, getStorageData, updateUserData } from '../../../modules/goldfishAdsRtdProvider'; + +const responseHeader = { 'Content-Type': 'application/json' }; + +const sampleConfig = { + name: 'golfishAds', + waitForIt: true, + params: { + key: 'testkey' + } +}; + +const sampleAdUnits = [ + { + code: 'one-div-id', + mediaTypes: { + banner: { + sizes: [970, 250] + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 12345370, + } + }] + }, + { + code: 'two-div-id', + mediaTypes: { + banner: { sizes: [300, 250] } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 12345370, + } + }] + }]; + +const sampleOutputData = [1, 2, 3] + +describe('goldfishAdsRtdProvider is a RTD provider that', function () { + describe('has a method `init` that', function () { + it('exists', function () { + expect(goldfishAdsSubModule.init).to.be.a('function'); + }); + it('returns false missing config params', function () { + const config = { + name: 'goldfishAds', + waitForIt: true, + }; + const value = goldfishAdsSubModule.init(config); + expect(value).to.equal(false); + }); + it('returns false if missing providers param', function () { + const config = { + name: 'goldfishAds', + waitForIt: true, + params: {} + }; + const value = goldfishAdsSubModule.init(config); + expect(value).to.equal(false); + }); + it('returns false if wrong providers param included', function () { + const config = { + name: 'goldfishAds', + waitForIt: true, + params: { + account: 'test' + } + }; + const value = goldfishAdsSubModule.init(config); + expect(value).to.equal(false); + }); + it('returns true if good providers param included', function () { + const config = { + name: 'goldfishAds', + waitForIt: true, + params: { + key: 'testkey' + } + }; + const value = goldfishAdsSubModule.init(config); + expect(value).to.equal(true); + }); + }); + + describe('has a method `getBidRequestData` that', function () { + it('exists', function () { + expect(goldfishAdsSubModule.getBidRequestData).to.be.a('function'); + }); + + it('send correct request', function () { + const callback = sinon.spy(); + let request; + const reqBidsConfigObj = { adUnits: sampleAdUnits }; + goldfishAdsSubModule.getBidRequestData(reqBidsConfigObj, callback, sampleConfig); + request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(sampleOutputData)); + expect(request.url).to.be.include(`?key=testkey`); + }); + }); + + describe('has a manageCallbackResponse that', function () { + it('properly transforms the response', function () { + const response = { response: '[\"1\", \"2\", \"3\"]' }; + const output = manageCallbackResponse(response); + expect(output.name).to.be.equal('goldfishads.com'); + }); + }); + + describe('has an updateUserData that', function () { + it('properly transforms the response', function () { + const userData = { + segment: [{id: '1'}, {id: '2'}], + ext: { + segtax: 4, + } + }; + const reqBidsConfigObj = { ortb2Fragments: { bidder: { appnexus: { user: { data: [] } } } } }; + const output = updateUserData(userData, reqBidsConfigObj); + expect(output.ortb2Fragments.bidder.appnexus.user.data[0].segment).to.be.length(2); + expect(output.ortb2Fragments.bidder.appnexus.user.data[0].segment[0].id).to.be.eql('1'); + }); + }); + + describe('uses Local Storage to ', function () { + const sandbox = sinon.createSandbox(); + const storage = getStorageManager({ moduleType: MODULE_TYPE, moduleName: MODULE_NAME }) + beforeEach(() => { + storage.setDataInLocalStorage(DATA_STORAGE_KEY, JSON.stringify({ + targeting: { + name: 'goldfishads.com', + segment: [{id: '1'}, {id: '2'}], + ext: { + segtax: 4, + } + }, + expiry: new Date().getTime() + 1000 * 60 * 60 * 24 * 30, + })); + }); + afterEach(() => { + sandbox.restore(); + }); + it('get data from local storage', function () { + const output = getStorageData(); + expect(output.name).to.be.equal('goldfishads.com'); + expect(output.segment).to.be.length(2); + expect(output.ext.segtax).to.be.equal(4); + }); + }); +}); diff --git a/test/spec/modules/gptPreAuction_spec.js b/test/spec/modules/gptPreAuction_spec.js index 2bfce71806e..fa2236f77c6 100644 --- a/test/spec/modules/gptPreAuction_spec.js +++ b/test/spec/modules/gptPreAuction_spec.js @@ -97,6 +97,18 @@ describe('GPT pre-auction module', () => { expect(adUnit.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: 'slotCode2' }); }); + it('should add adServer object to context if matching slot is found (in case of twin ad unit)', () => { + window.googletag.pubads().setSlots(testSlots); + const adUnit1 = { code: 'slotCode2', ortb2Imp: { ext: { data: {} } } }; + const adUnit2 = { code: 'slotCode2', ortb2Imp: { ext: { data: {} } } }; + appendGptSlots([adUnit1, adUnit2]); + expect(adUnit1.ortb2Imp.ext.data.adserver).to.be.an('object'); + expect(adUnit1.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: 'slotCode2' }); + + expect(adUnit2.ortb2Imp.ext.data.adserver).to.be.an('object'); + expect(adUnit2.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: 'slotCode2' }); + }); + it('will trim child id if mcmEnabled is set to true', () => { config.setConfig({ gptPreAuction: { enabled: true, mcmEnabled: true } }); window.googletag.pubads().setSlots([ diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index 870fbd23870..30361ca5661 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -1,8 +1,11 @@ import { - greenbidsAnalyticsAdapter, parseBidderCode, + greenbidsAnalyticsAdapter, + isSampled, ANALYTICS_VERSION, BIDDER_STATUS } from 'modules/greenbidsAnalyticsAdapter.js'; - +import { + generateUUID, +} from '../../../src/utils.js'; import {expect} from 'chai'; import sinon from 'sinon'; @@ -13,11 +16,42 @@ const pbuid = 'pbuid-AA778D8A796AEA7A0843E2BBEB677766'; const auctionId = 'b0b39610-b941-4659-a87c-de9f62d3e13e'; describe('Greenbids Prebid AnalyticsAdapter Testing', function () { + describe('enableAnalytics and config parser', function () { + const configOptions = { + pbuid: pbuid, + greenbidsSampling: 1, + }; + beforeEach(function () { + greenbidsAnalyticsAdapter.enableAnalytics({ + provider: 'greenbidsAnalytics', + options: configOptions + }); + }); + + afterEach(function () { + greenbidsAnalyticsAdapter.disableAnalytics(); + }); + + it('should parse config correctly with optional values', function () { + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().options).to.deep.equal(configOptions); + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().pbuid).to.equal(configOptions.pbuid); + }); + + it('should not enable Analytics when pbuid is missing', function () { + const configOptions = { + options: { + } + }; + const validConfig = greenbidsAnalyticsAdapter.initConfig(configOptions); + expect(validConfig).to.equal(false); + }); + }); + describe('event tracking and message cache manager', function () { beforeEach(function () { const configOptions = { pbuid: pbuid, - sampling: 0, + greenbidsSampling: 1, }; greenbidsAnalyticsAdapter.enableAnalytics({ @@ -30,43 +64,6 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { greenbidsAnalyticsAdapter.disableAnalytics(); }); - describe('#parseBidderCode()', function() { - it('should get lower case bidder code from bidderCode field value', function() { - const receivedBids = [ - { - auctionId: auctionId, - adUnitCode: 'adunit_1', - bidder: 'greenbids', - bidderCode: 'GREENBIDS', - requestId: 'a1b2c3d4', - timeToRespond: 72, - cpm: 0.1, - currency: 'USD', - ad: 'fake ad1' - }, - ]; - const result = parseBidderCode(receivedBids[0]); - expect(result).to.equal('greenbids'); - }); - it('should get lower case bidder code from bidder field value as bidderCode field is missing', function() { - const receivedBids = [ - { - auctionId: auctionId, - adUnitCode: 'adunit_1', - bidder: 'greenbids', - bidderCode: '', - requestId: 'a1b2c3d4', - timeToRespond: 72, - cpm: 0.1, - currency: 'USD', - ad: 'fake ad1' - }, - ]; - const result = parseBidderCode(receivedBids[0]); - expect(result).to.equal('greenbids'); - }); - }); - describe('#getCachedAuction()', function() { const existing = {timeoutBids: [{}]}; greenbidsAnalyticsAdapter.cachedAuctions['test_auction_id'] = existing; @@ -146,7 +143,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { auctionId: auctionId, pbuid: pbuid, referrer: window.location.href, - sampling: 0, + sampling: 1, prebid: '$prebid.version$', }); } @@ -246,6 +243,13 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { skip: 1, protocols: [1, 2, 3, 4] }, + }, + ortb2Imp: { + ext: { + data: { + adunitDFP: 'adunitcustomPathExtension' + } + } } }, ], @@ -253,7 +257,9 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { noBids: noBids }; + sinon.stub(greenbidsAnalyticsAdapter, 'getCachedAuction').returns({timeoutBids: timeoutBids}); const result = greenbidsAnalyticsAdapter.createBidMessage(args, timeoutBids); + greenbidsAnalyticsAdapter.getCachedAuction.restore(); assertHavingRequiredMessageFields(result); expect(result).to.deep.include({ @@ -266,6 +272,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { sizes: [[300, 250], [300, 600]] } }, + ortb2Imp: {}, bidders: [ { bidder: 'greenbids', @@ -281,6 +288,13 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }, { code: 'adunit-2', + ortb2Imp: { + ext: { + data: { + adunitDFP: 'adunitcustomPathExtension' + } + } + }, mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] @@ -315,7 +329,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { timeout: 3000, auctionEnd: 1234567990, bidsReceived: receivedBids, - noBids: noBids + noBids: noBids, }]; greenbidsAnalyticsAdapter.handleBidTimeout(args); @@ -338,7 +352,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { describe('greenbids Analytics Adapter track handler ', function () { const configOptions = { pbuid: pbuid, - sampling: 1, + greenbidsSampling: 1, }; beforeEach(function () { @@ -354,50 +368,52 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { events.getEvents.restore(); }); + it('should call handleAuctionInit as AUCTION_INIT trigger event', function() { + sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionInit'); + events.emit(constants.EVENTS.AUCTION_INIT, {auctionId: 'auctionId'}); + sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionInit, 1); + greenbidsAnalyticsAdapter.handleAuctionInit.restore(); + }); + it('should call handleBidTimeout as BID_TIMEOUT trigger event', function() { sinon.spy(greenbidsAnalyticsAdapter, 'handleBidTimeout'); - events.emit(constants.EVENTS.BID_TIMEOUT, {}); + events.emit(constants.EVENTS.BID_TIMEOUT, {auctionId: 'auctionId'}); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBidTimeout, 1); greenbidsAnalyticsAdapter.handleBidTimeout.restore(); }); it('should call handleAuctionEnd as AUCTION_END trigger event', function() { sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionEnd'); - events.emit(constants.EVENTS.AUCTION_END, {}); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: 'auctionId'}); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionEnd, 1); greenbidsAnalyticsAdapter.handleAuctionEnd.restore(); }); - }); - - describe('enableAnalytics and config parser', function () { - const configOptions = { - pbuid: pbuid, - sampling: 0, - }; - beforeEach(function () { - greenbidsAnalyticsAdapter.enableAnalytics({ - provider: 'greenbidsAnalytics', - options: configOptions + it('should call handleBillable as BILLABLE_EVENT trigger event', function() { + sinon.spy(greenbidsAnalyticsAdapter, 'handleBillable'); + events.emit(constants.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: 'auctionId', + vendor: 'greenbidsRtdProvider' }); + sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBillable, 1); + greenbidsAnalyticsAdapter.handleBillable.restore(); }); + }); - afterEach(function () { - greenbidsAnalyticsAdapter.disableAnalytics(); + 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; }); - it('should parse config correctly with optional values', function () { - expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().options).to.deep.equal(configOptions); - expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().pbuid).to.equal(configOptions.pbuid); + 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; }); - it('should not enable Analytics when pbuid is missing', function () { - const configOptions = { - options: { - } - }; - const validConfig = greenbidsAnalyticsAdapter.initConfig(configOptions); - expect(validConfig).to.equal(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; }); }); }); diff --git a/test/spec/modules/greenbidsRtdProvider_spec.js b/test/spec/modules/greenbidsRtdProvider_spec.js index cd93e9013c0..d0083d4dc7a 100644 --- a/test/spec/modules/greenbidsRtdProvider_spec.js +++ b/test/spec/modules/greenbidsRtdProvider_spec.js @@ -6,7 +6,9 @@ import { import { greenbidsSubmodule } from 'modules/greenbidsRtdProvider.js'; -import {server} from '../../mocks/xhr.js'; +import { server } from '../../mocks/xhr.js'; +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; describe('greenbidsRtdProvider', () => { const endPoint = 't.greenbids.ai'; @@ -39,14 +41,36 @@ describe('greenbidsRtdProvider', () => { }] }; - const SAMPLE_RESPONSE_ADUNITS = [ + const SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED = [ { code: 'adUnit1', bidders: { 'appnexus': true, 'rubicon': false, 'ix': true - } + }, + isExploration: false + }, + { + code: 'adUnit2', + bidders: { + 'appnexus': false, + 'rubicon': true, + 'openx': true + }, + isExploration: false + + }]; + + const SAMPLE_RESPONSE_ADUNITS_EXPLORED = [ + { + code: 'adUnit1', + bidders: { + 'appnexus': true, + 'rubicon': false, + 'ix': true + }, + isExploration: true }, { code: 'adUnit2', @@ -54,7 +78,9 @@ describe('greenbidsRtdProvider', () => { 'appnexus': false, 'rubicon': true, 'openx': true - } + }, + isExploration: true + }]; describe('init', () => { @@ -70,22 +96,37 @@ describe('greenbidsRtdProvider', () => { }); describe('updateAdUnitsBasedOnResponse', () => { - it('should update ad units based on response', () => { + it('should update ad units based on response if not exploring', () => { const adUnits = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits)); - greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS); + greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED); expect(adUnits[0].bids).to.have.length(2); expect(adUnits[1].bids).to.have.length(2); }); + + it('should not update ad units based on response if exploring', () => { + const adUnits = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits)); + greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS_EXPLORED); + + expect(adUnits[0].bids).to.have.length(3); + expect(adUnits[1].bids).to.have.length(3); + expect(adUnits[0].ortb2Imp.ext.greenbids.greenbidsId).to.be.a.string; + expect(adUnits[1].ortb2Imp.ext.greenbids.greenbidsId).to.be.a.string; + expect(adUnits[0].ortb2Imp.ext.greenbids.greenbidsId).to.equal(adUnits[0].ortb2Imp.ext.greenbids.greenbidsId); + expect(adUnits[0].ortb2Imp.ext.greenbids.keptInAuction).to.deep.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[0].bidders); + expect(adUnits[1].ortb2Imp.ext.greenbids.keptInAuction).to.deep.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[1].bidders); + expect(adUnits[0].ortb2Imp.ext.greenbids.isExploration).to.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[0].isExploration); + expect(adUnits[1].ortb2Imp.ext.greenbids.isExploration).to.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[1].isExploration); + }); }); describe('findMatchingAdUnit', () => { it('should find matching ad unit by code', () => { - const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'adUnit1'); - expect(matchingAdUnit).to.deep.equal(SAMPLE_RESPONSE_ADUNITS[0]); + const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED, 'adUnit1'); + expect(matchingAdUnit).to.deep.equal(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED[0]); }); it('should return undefined if no matching ad unit is found', () => { - const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'nonexistent'); + const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED, 'nonexistent'); expect(matchingAdUnit).to.be.undefined; }); }); @@ -93,7 +134,7 @@ describe('greenbidsRtdProvider', () => { describe('removeFalseBidders', () => { it('should remove bidders with false value', () => { const adUnit = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits[0])); - const matchingAdUnit = SAMPLE_RESPONSE_ADUNITS[0]; + const matchingAdUnit = SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED[0]; greenbidsSubmodule.removeFalseBidders(adUnit, matchingAdUnit); expect(adUnit.bids).to.have.length(2); expect(adUnit.bids.map((bid) => bid.bidder)).to.not.include('rubicon'); @@ -125,14 +166,15 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { server.requests[0].respond( 200, - {'Content-Type': 'application/json'}, - JSON.stringify(SAMPLE_RESPONSE_ADUNITS) + { 'Content-Type': 'application/json' }, + JSON.stringify(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED) ); }, 50); setTimeout(() => { const requestUrl = new URL(server.requests[0].url); expect(requestUrl.host).to.be.eq(endPoint); + expect(requestBids.greenbidsId).to.be.a.string; expect(requestBids.adUnits[0].bids).to.have.length(2); expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.not.include('rubicon'); expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.include('ix'); @@ -157,8 +199,8 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { server.requests[0].respond( 200, - {'Content-Type': 'application/json'}, - JSON.stringify(SAMPLE_RESPONSE_ADUNITS) + { 'Content-Type': 'application/json' }, + JSON.stringify(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED) ); done(); }, 300); @@ -166,6 +208,7 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { const requestUrl = new URL(server.requests[0].url); expect(requestUrl.host).to.be.eq(endPoint); + expect(requestBids.greenbidsId).to.be.a.string; expect(requestBids.adUnits[0].bids).to.have.length(3); expect(requestBids.adUnits[1].bids).to.have.length(3); expect(callback.calledOnce).to.be.true; @@ -183,14 +226,15 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { server.requests[0].respond( 500, - {'Content-Type': 'application/json'}, - JSON.stringify({'failure': 'fail'}) + { 'Content-Type': 'application/json' }, + JSON.stringify({ 'failure': 'fail' }) ); }, 50); setTimeout(() => { const requestUrl = new URL(server.requests[0].url); expect(requestUrl.host).to.be.eq(endPoint); + expect(requestBids.greenbidsId).to.be.a.string; expect(requestBids.adUnits[0].bids).to.have.length(3); expect(requestBids.adUnits[1].bids).to.have.length(3); expect(callback.calledOnce).to.be.true; @@ -198,4 +242,116 @@ describe('greenbidsRtdProvider', () => { }, 60); }); }); + + describe('stripAdUnits', function () { + it('should strip all properties except bidder from each bid in adUnits', function () { + const adUnits = + [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } } + } + ]; + const expectedOutput = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } } + } + ]; + + // Perform the test + const output = greenbidsSubmodule.stripAdUnits(adUnits); + expect(output).to.deep.equal(expectedOutput); + }); + + it('should strip all properties except bidder from each bid in adUnits but keep ortb2Imp', function () { + const adUnits = + [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } }, + ortb2Imp: { + ext: { + greenbids: { + greenbidsId: 'test' + } + } + } + } + ]; + const expectedOutput = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } }, + ortb2Imp: { + ext: { + greenbids: { + greenbidsId: 'test' + } + } + } + } + ]; + + // Perform the test + const output = greenbidsSubmodule.stripAdUnits(adUnits); + expect(output).to.deep.equal(expectedOutput); + }); + }); + + describe('onAuctionInitEvent', function () { + it('should not emit billable event if greenbids hasn\'t set the adunit.ext value', function () { + sinon.spy(events, 'emit'); + greenbidsSubmodule.onAuctionInitEvent({ + auctionId: 'test', + adUnits: [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } }, + } + ] + }); + sinon.assert.callCount(events.emit, 0); + events.emit.restore(); + }); + + it('should emit billable event if greenbids has set the adunit.ext value', function (done) { + let counter = 0; + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, function (event) { + if (event.vendor === 'greenbidsRtdProvider' && event.type === 'auction') { + counter += 1; + } + expect(counter).to.equal(1); + done(); + }); + greenbidsSubmodule.onAuctionInitEvent({ + auctionId: 'test', + adUnits: [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } }, + ortb2Imp: { ext: { greenbids: { greenbidsId: 'b0b39610-b941-4659-a87c-de9f62d3e13e' } } } + } + ] + }); + }); + }); }); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index da51ed058be..0611fa68bf8 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -941,6 +941,15 @@ describe('TheMediaGrid Adapter', function () { getDataFromLocalStorageStub.restore(); }) + it('tmax should be set as integer', function() { + let [request] = spec.buildRequests([bidRequests[0]], {...bidderRequest, timeout: '10'}); + let payload = parseRequest(request.data); + expect(payload.tmax).to.equal(10); + [request] = spec.buildRequests([bidRequests[0]], {...bidderRequest, timeout: 'ddqwdwdq'}); + payload = parseRequest(request.data); + expect(payload.tmax).to.equal(null); + }) + describe('floorModule', function () { const floorTestData = { 'currency': 'USD', @@ -975,6 +984,15 @@ describe('TheMediaGrid Adapter', function () { const payload = parseRequest(request.data); expect(payload.imp[0].bidfloor).to.equal(bidfloor); }); + it('should return the bidfloor string value if it is greater than getFloor.floor', function () { + const bidfloor = '1.80'; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const [request] = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(1.80); + }); }); }); @@ -1541,37 +1559,9 @@ describe('TheMediaGrid Adapter', function () { }); it('should send right request on onDataDeletionRequest call', function() { - spec.onDataDeletionRequest([{ - bids: [ - { - bidder: 'grid', - params: { - uid: 1 - } - }, - { - bidder: 'grid', - params: { - uid: 2 - } - }, - { - bidder: 'another', - params: { - uid: 3 - } - }, - { - bidder: 'gridNM', - params: { - uid: 4 - } - } - ], - }]); + spec.onDataDeletionRequest([{}]); expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal('https://media.grid.bidswitch.net/uspapi_delete'); - expect(ajaxStub.firstCall.args[2]).to.equal('{"uids":[1,2,4]}'); + expect(ajaxStub.firstCall.args[0]).to.equal('https://media.grid.bidswitch.net/uspapi_delete_c2s'); }); }); diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 0e64ec67b27..e0529a895f5 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -102,6 +102,8 @@ describe('gumgumAdapter', function () { let sizesArray = [[300, 250], [300, 600]]; let bidRequests = [ { + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + gppSid: [7], bidder: 'gumgum', params: { inSlot: 9 @@ -119,6 +121,30 @@ describe('gumgumAdapter', function () { } } }, + pubProvidedId: [ + { + uids: [ + { + ext: { + stype: 'ppuid', + }, + id: 'aac4504f-ef89-401b-a891-ada59db44336', + }, + ], + source: 'sonobi.com', + }, + { + uids: [ + { + ext: { + stype: 'ppuid', + }, + id: 'y-zqTHmW9E2uG3jEETC6i6BjGcMhPXld2F~A', + }, + ], + source: 'aol.com', + }, + ], adUnitCode: 'adunit-code', sizes: sizesArray, bidId: '30b31c1838de1e', @@ -155,6 +181,7 @@ describe('gumgumAdapter', function () { linearity: 1, startdelay: 1, placement: 123456, + plcmt: 3, protocols: [1, 2] } }; @@ -166,6 +193,11 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.aun).to.equal(bidRequests[0].adUnitCode); }); + it('should set pubProvidedId if the uid and pubProvidedId are available', function () { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pubProvidedId).to.equal(JSON.stringify(bidRequests[0].userId.pubProvidedId)); + }); it('should set id5Id and id5IdLinkType if the uid and linkType are available', function () { const request = { ...bidRequests[0] }; const bidRequest = spec.buildRequests([request])[0]; @@ -265,6 +297,14 @@ describe('gumgumAdapter', function () { expect(bidRequest.data.gpid).to.equal(pbadslot); }); + it('should set the global placement id (gpid) if media type is video', function () { + const pbadslot = 'cde456' + const req = { ...bidRequests[0], ortb2Imp: { ext: { data: { pbadslot } } }, params: zoneParam, mediaTypes: vidMediaTypes } + const bidRequest = spec.buildRequests([req])[0]; + expect(bidRequest.data).to.have.property('gpid'); + expect(bidRequest.data.gpid).to.equal(pbadslot); + }); + it('should set the bid floor if getFloor module is not present but static bid floor is defined', function () { const req = { ...bidRequests[0], params: { bidfloor: 42 } } const bidRequest = spec.buildRequests([req])[0]; @@ -426,6 +466,7 @@ describe('gumgumAdapter', function () { linearity: 1, startdelay: 1, placement: 123456, + plcmt: 3, protocols: [1, 2] }; const request = Object.assign({}, bidRequests[0]); @@ -444,6 +485,7 @@ describe('gumgumAdapter', function () { expect(bidRequest.data.li).to.eq(videoVals.linearity); expect(bidRequest.data.sd).to.eq(videoVals.startdelay); expect(bidRequest.data.pt).to.eq(videoVals.placement); + expect(bidRequest.data.vplcmt).to.eq(videoVals.plcmt); expect(bidRequest.data.pr).to.eq(videoVals.protocols.join(',')); expect(bidRequest.data.viw).to.eq(videoVals.playerSize[0].toString()); expect(bidRequest.data.vih).to.eq(videoVals.playerSize[1].toString()); @@ -457,6 +499,7 @@ describe('gumgumAdapter', function () { linearity: 1, startdelay: 1, placement: 123456, + plcmt: 3, protocols: [1, 2] }; const request = Object.assign({}, bidRequests[0]); @@ -475,6 +518,7 @@ describe('gumgumAdapter', function () { expect(bidRequest.data.li).to.eq(inVideoVals.linearity); expect(bidRequest.data.sd).to.eq(inVideoVals.startdelay); expect(bidRequest.data.pt).to.eq(inVideoVals.placement); + expect(bidRequest.data.vplcmt).to.eq(inVideoVals.plcmt); expect(bidRequest.data.pr).to.eq(inVideoVals.protocols.join(',')); expect(bidRequest.data.viw).to.eq(inVideoVals.playerSize[0].toString()); expect(bidRequest.data.vih).to.eq(inVideoVals.playerSize[1].toString()); @@ -486,6 +530,12 @@ describe('gumgumAdapter', function () { expect(request.data).to.not.include.any.keys('eAdBuyId'); expect(request.data).to.not.include.any.keys('adBuyId'); }); + it('should set pubProvidedId if the uid and pubProvidedId are available', function () { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pubProvidedId).to.equal(JSON.stringify(bidRequests[0].userId.pubProvidedId)); + }); + it('should add gdpr consent parameters if gdprConsent is present', function () { const gdprConsent = { consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', gdprApplies: true }; const fakeBidRequest = { gdprConsent: gdprConsent }; @@ -503,10 +553,9 @@ describe('gumgumAdapter', function () { const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [7] } const fakeBidRequest = { gppConsent: gppConsent }; const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; - expect(bidRequest.data.gppConsent).to.exist; - expect(bidRequest.data.gppConsent.gppString).to.equal(gppConsent.gppString); - expect(bidRequest.data.gppConsent.gpp_sid).to.equal(gppConsent.applicableSections); - expect(bidRequest.data.gppConsent.gppString).to.eq('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(bidRequest.data.gppString).to.equal(gppConsent.gppString); + expect(bidRequest.data.gppSid).to.equal(gppConsent.applicableSections.join(',')); + expect(bidRequest.data.gppString).to.eq('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); }); it('should handle ortb2 parameters', function () { const ortb2 = { @@ -517,15 +566,14 @@ describe('gumgumAdapter', function () { } const fakeBidRequest = { gppConsent: ortb2 }; const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; - expect(bidRequest.data.gppConsent.gppString).to.eq(fakeBidRequest[0]) + expect(bidRequest.data.gpp).to.eq(fakeBidRequest[0]) }); it('should handle gppConsent is present but values are undefined case', function () { const gppConsent = { gppString: undefined, applicableSections: undefined } const fakeBidRequest = { gppConsent: gppConsent }; const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; - expect(bidRequest.data.gppConsent).to.exist; - expect(bidRequest.data.gppConsent.gppString).to.equal(undefined); - expect(bidRequest.data.gppConsent.gpp_sid).to.equal(undefined); + expect(bidRequest.data.gppString).to.equal(''); + expect(bidRequest.data.gppSid).to.equal(''); }); it('should handle ortb2 undefined parameters', function () { const ortb2 = { @@ -536,8 +584,8 @@ describe('gumgumAdapter', function () { } const fakeBidRequest = { gppConsent: ortb2 }; const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; - expect(bidRequest.data.gppConsent.gppString).to.eq(undefined) - expect(bidRequest.data.gppConsent.gpp_sid).to.eq(undefined) + expect(bidRequest.data.gppString).to.eq('') + expect(bidRequest.data.gppSid).to.eq('') }); it('should not set coppa parameter if coppa config is set to false', function () { config.setConfig({ diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js index cc0118d4659..85c8cc11c9e 100644 --- a/test/spec/modules/hadronIdSystem_spec.js +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -22,7 +22,7 @@ describe('HadronIdSystem', function () { const callback = hadronIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq(`https://id.hadron.ad.gt/api/v1/pbhid?partner_id=0&_it=prebid`); + expect(request.url).to.match(/^https:\/\/id\.hadron\.ad\.gt\/api\/v1\/pbhid/); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); @@ -47,7 +47,7 @@ describe('HadronIdSystem', function () { const callback = hadronIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://hadronid.publync.com/?partner_id=0&_it=prebid'); + expect(request.url).to.match(/^https:\/\/hadronid\.publync\.com\//); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 56b23ba9634..d3d91db010d 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -15,7 +15,7 @@ import {config} from 'src/config.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; -import {uspDataHandler} from 'src/adapterManager.js'; +import {uspDataHandler, gppDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; @@ -259,12 +259,14 @@ describe('ID5 ID System', function () { describe('Xhr Requests from getId()', function () { const responseHeader = HEADERS_CONTENT_TYPE_JSON + let gppStub beforeEach(function () { }); afterEach(function () { uspDataHandler.reset() + gppStub?.restore() }); it('should call the ID5 server and handle a valid response', function () { @@ -730,6 +732,26 @@ describe('ID5 ID System', function () { }); }); + it('should pass gpp_string and gpp_sid to ID5 server', function () { + let xhrServerMock = new XhrServerMock(server) + gppStub = sinon.stub(gppDataHandler, 'getConsentData'); + gppStub.returns({ + ready: true, + gppString: 'GPP_STRING', + applicableSections: [2] + }); + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.gpp_string).is.equal('GPP_STRING'); + expect(requestBody.gpp_sid).contains(2); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }); + }); + describe('when legacy cookies are set', () => { let sandbox; beforeEach(() => { diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index a273f26b28b..66d5a3edd00 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -3,6 +3,7 @@ import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; import {getCoreStorageManager} from '../../../src/storageManager.js'; import {stub} from 'sinon'; +import { gppDataHandler } from '../../../src/adapterManager.js'; const storage = getCoreStorageManager(); @@ -20,6 +21,7 @@ function setTestEnvelopeCookie () { describe('IdentityLinkId tests', function () { let logErrorStub; + let gppConsentDataStub; beforeEach(function () { defaultConfigParams = { params: {pid: pid} }; @@ -73,16 +75,19 @@ describe('IdentityLinkId tests', function () { expect(submoduleCallback).to.be.undefined; }); - it('should call the LiveRamp envelope endpoint with IAB consent string v1', function () { + it('should call the LiveRamp envelope endpoint with IAB consent string v2', function () { let callBackSpy = sinon.spy(); let consentData = { gdprApplies: true, - consentString: 'BOkIpDSOkIpDSADABAENCc-AAAApOAFAAMAAsAMIAcAA_g' + consentString: 'CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA', + vendorData: { + tcfPolicyVersion: 2 + } }; let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=1&cv=BOkIpDSOkIpDSADABAENCc-AAAApOAFAAMAAsAMIAcAA_g'); + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=4&cv=CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA'); request.respond( 200, responseHeader, @@ -91,25 +96,46 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveRamp envelope endpoint with IAB consent string v2', function () { + it('should call the LiveRamp envelope endpoint with GPP consent string', function() { + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + gppConsentDataStub.returns({ + ready: true, + gppString: 'DBABLA~BVVqAAAACqA.QA', + applicableSections: [7] + }); let callBackSpy = sinon.spy(); - let consentData = { - gdprApplies: true, - consentString: 'CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA', - vendorData: { - tcfPolicyVersion: 2 - } - }; - let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData).callback; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=4&cv=CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA'); + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&gpp=DBABLA~BVVqAAAACqA.QA&gpp_sid=7'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + gppConsentDataStub.restore(); + }); + + it('should call the LiveRamp envelope endpoint without GPP consent string if consent string is not provided', function () { + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); + gppConsentDataStub.returns({ + ready: true, + gppString: '', + applicableSections: [7] + }); + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14'); request.respond( 200, responseHeader, JSON.stringify({}) ); expect(callBackSpy.calledOnce).to.be.true; + gppConsentDataStub.restore(); }); it('should not throw Uncaught TypeError when envelope endpoint returns empty response', function () { diff --git a/test/spec/modules/illuminBidAdapter_spec.js b/test/spec/modules/illuminBidAdapter_spec.js new file mode 100644 index 00000000000..9b702c027f9 --- /dev/null +++ b/test/spec/modules/illuminBidAdapter_spec.js @@ -0,0 +1,634 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/illuminBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppString': 'gpp_string', + 'gppSid': [7], + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['illumin.com'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('IlluminBidAdapter', function () { + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + illumin: { + storageAllowed: true + } + }; + sandbox = sinon.sandbox.create(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + prebidVersion: version, + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + }, + gpid: '0123456789' + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.illumin.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.illumin.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }) + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['illumin.com'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['illumin.com'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['illumin.com'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'lipb': + return {lipbid: id}; + case 'parrableId': + return {eid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cId': '1'}); + const pid = extractPID({'pId': '2'}); + const subDomain = extractSubDomain({'subDomain': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + illumin: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + illumin: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem('myKey', 2020); + const {value, created} = getStorageItem('myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem('myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); diff --git a/test/spec/modules/imdsBidAdapter_spec.js b/test/spec/modules/imdsBidAdapter_spec.js index 7d808a2528f..b71a0bc51d9 100644 --- a/test/spec/modules/imdsBidAdapter_spec.js +++ b/test/spec/modules/imdsBidAdapter_spec.js @@ -1362,17 +1362,17 @@ describe('imdsBidAdapter ', function () { expect(usersyncs[0].url).to.contain('https://ad-cdn.technoratimedia.com/html/usersync.html'); }); - it('should return a pixel usersync when pixels is enabled', function () { + it('should return an image usersync when pixels are enabled', function () { let usersyncs = spec.getUserSyncs({ pixelEnabled: true }, null); expect(usersyncs).to.be.an('array').with.lengthOf(1); - expect(usersyncs[0]).to.have.property('type', 'pixel'); + expect(usersyncs[0]).to.have.property('type', 'image'); expect(usersyncs[0]).to.have.property('url'); expect(usersyncs[0].url).to.contain('https://sync.technoratimedia.com/services'); }); - it('should return an iframe usersync when both iframe and pixels is enabled', function () { + it('should return an iframe usersync when both iframe and pixel are enabled', function () { let usersyncs = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true diff --git a/test/spec/modules/impactifyBidAdapter_spec.js b/test/spec/modules/impactifyBidAdapter_spec.js index 215972ff450..d9bf4becb22 100644 --- a/test/spec/modules/impactifyBidAdapter_spec.js +++ b/test/spec/modules/impactifyBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; -import { spec } from 'modules/impactifyBidAdapter.js'; +import { spec, STORAGE, STORAGE_KEY } from 'modules/impactifyBidAdapter.js'; import * as utils from 'src/utils.js'; +import sinon from 'sinon'; const BIDDER_CODE = 'impactify'; const BIDDER_ALIAS = ['imp']; @@ -19,89 +20,202 @@ var gdprData = { }; describe('ImpactifyAdapter', function () { + let getLocalStorageStub; + let localStorageIsEnabledStub; + let sandbox; + + beforeEach(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + impactify: { + storageAllowed: true + } + }; + sinon.stub(document.body, 'appendChild'); + sandbox = sinon.sandbox.create(); + getLocalStorageStub = sandbox.stub(STORAGE, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sandbox.stub(STORAGE, 'localStorageIsEnabled'); + }); + + afterEach(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + document.body.appendChild.restore(); + sandbox.restore(); + }); + describe('isBidRequestValid', function () { - let validBid = { - bidder: 'impactify', - params: { - appId: '1', - format: 'screen', - style: 'inline' + let validBids = [ + { + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'screen', + style: 'inline' + } + }, + { + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'display', + style: 'static' + } + } + ]; + + let videoBidRequests = [ + { + bidder: 'impactify', + params: { + appId: '1', + format: 'screen', + style: 'inline' + }, + mediaTypes: { + video: { + context: 'instream' + } + }, + adUnitCode: 'adunit-code', + sizes: [[DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT]], + bidId: '123456789', + bidderRequestId: '987654321', + auctionId: '19ab94a9-b0d7-4ed7-9f80-ad0c033cf1b1', + transactionId: 'f7b2c372-7a7b-11eb-9439-0242ac130002', + userId: { + pubcid: '87a0327b-851c-4bb3-a925-0c7be94548f5' + }, + userIdAsEids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '87a0327b-851c-4bb3-a925-0c7be94548f5', + atype: 1 + } + ] + } + ] + } + ]; + let videoBidderRequest = { + bidderRequestId: '98845765110', + auctionId: '165410516454', + bidderCode: 'impactify', + bids: [ + { + ...videoBidRequests[0] + } + ], + refererInfo: { + referer: 'https://impactify.io' } }; it('should return true when required params found', function () { - expect(spec.isBidRequestValid(validBid)).to.equal(true); + expect(spec.isBidRequestValid(validBids[0])).to.equal(true); + expect(spec.isBidRequestValid(validBids[1])).to.equal(true); }); it('should return false when required params are not passed', function () { - let bid = Object.assign({}, validBid); + let bid = Object.assign({}, validBids[0]); delete bid.params; bid.params = {}; expect(spec.isBidRequestValid(bid)).to.equal(false); + + let bid2 = Object.assign({}, validBids[1]); + delete bid2.params; + bid2.params = {}; + expect(spec.isBidRequestValid(bid2)).to.equal(false); }); it('should return false when appId is missing', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); delete bid.params.appId; - expect(spec.isBidRequestValid(bid)).to.equal(false); + + const bid2 = utils.deepClone(validBids[1]); + delete bid2.params.appId; + expect(spec.isBidRequestValid(bid2)).to.equal(false); }); it('should return false when appId is not a string', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); + const bid2 = utils.deepClone(validBids[1]); bid.params.appId = 123; + bid2.params.appId = 123; expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); bid.params.appId = false; + bid2.params.appId = false; expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); bid.params.appId = void (0); + bid2.params.appId = void (0); expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); bid.params.appId = {}; + bid2.params.appId = {}; expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); }); it('should return false when format is missing', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); delete bid.params.format; expect(spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when format is not a string', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); + const bid2 = utils.deepClone(validBids[1]); bid.params.format = 123; + bid2.params.format = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); expect(spec.isBidRequestValid(bid)).to.equal(false); bid.params.format = false; + bid2.params.format = false; expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); bid.params.format = void (0); + bid2.params.format = void (0); expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); bid.params.format = {}; + bid2.params.format = {}; expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(spec.isBidRequestValid(bid2)).to.equal(false); }); it('should return false when format is not equals to screen or display', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); if (bid.params.format != 'screen' && bid.params.format != 'display') { expect(spec.isBidRequestValid(bid)).to.equal(false); } + + const bid2 = utils.deepClone(validBids[1]); + if (bid2.params.format != 'screen' && bid2.params.format != 'display') { + expect(spec.isBidRequestValid(bid2)).to.equal(false); + } }); it('should return false when style is missing', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); delete bid.params.style; expect(spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when style is not a string', () => { - const bid = utils.deepClone(validBid); + const bid = utils.deepClone(validBids[0]); bid.params.style = 123; expect(spec.isBidRequestValid(bid)).to.equal(false); @@ -167,22 +281,44 @@ describe('ImpactifyAdapter', function () { }; it('should pass bidfloor', function () { - videoBidRequests[0].getFloor = function() { + videoBidRequests[0].getFloor = function () { return { currency: 'USD', floor: 1.23, } } - const res = spec.buildRequests(videoBidRequests, videoBidderRequest) + const res = spec.buildRequests(videoBidRequests, videoBidderRequest); const resData = JSON.parse(res.data) expect(resData.imp[0].bidfloor).to.equal(1.23) }); it('sends video bid request to ENDPOINT via POST', function () { + localStorageIsEnabledStub.returns(true); + + getLocalStorageStub.returns('testValue'); + const request = spec.buildRequests(videoBidRequests, videoBidderRequest); + expect(request.url).to.equal(ORIGIN + AUCTIONURI); expect(request.method).to.equal('POST'); + expect(request.options.customHeaders['x-impact']).to.equal('testValue'); + }); + + it('should set header value from localstorage correctly', function () { + localStorageIsEnabledStub.returns(true); + getLocalStorageStub.returns('testValue'); + + const request = spec.buildRequests(videoBidRequests, videoBidderRequest); + expect(request.options.customHeaders).to.be.an('object'); + expect(request.options.customHeaders['x-impact']).to.equal('testValue'); + }); + + it('should set header value to empty if localstorage is not enabled', function () { + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(videoBidRequests, videoBidderRequest); + expect(request.options.customHeaders).to.be.undefined; }); }); describe('interpretResponse', function () { @@ -205,7 +341,7 @@ describe('ImpactifyAdapter', function () { h: 1, hash: 'test', expiry: 166192938, - meta: {'advertiserDomains': ['testdomain.com']}, + meta: { 'advertiserDomains': ['testdomain.com'] }, ext: { prebid: { 'type': 'video' @@ -281,7 +417,7 @@ describe('ImpactifyAdapter', function () { height: 1, hash: 'test', expiry: 166192938, - meta: {'advertiserDomains': ['testdomain.com']}, + meta: { 'advertiserDomains': ['testdomain.com'] }, ttl: 300, creativeId: '97517771' } @@ -343,7 +479,7 @@ describe('ImpactifyAdapter', function () { h: 1, hash: 'test', expiry: 166192938, - meta: {'advertiserDomains': ['testdomain.com']}, + meta: { 'advertiserDomains': ['testdomain.com'] }, ext: { prebid: { 'type': 'video' @@ -399,8 +535,8 @@ describe('ImpactifyAdapter', function () { const result = spec.getUserSyncs('bad', [], gdprData); expect(result).to.be.empty; }); - it('should append the various values if they exist', function() { - const result = spec.getUserSyncs({iframeEnabled: true}, validResponse, gdprData); + it('should append the various values if they exist', function () { + const result = spec.getUserSyncs({ iframeEnabled: true }, validResponse, gdprData); expect(result[0].url).to.include('gdpr=1'); expect(result[0].url).to.include('gdpr_consent=BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAAAAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA'); }); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index f427f9e7624..a86b9be73e6 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1181,6 +1181,16 @@ describe('Improve Digital Adapter Tests', function () { expect(bids[0].dealId).to.equal(268515); }); + it('should set deal type targeting KV for PG', function () { + const request = makeRequest(bidderRequest); + const response = deepClone(serverResponse); + let bids; + + response.body.seatbid[0].bid[0].ext.improvedigital.pg = 1; + bids = spec.interpretResponse(response, request); + expect(bids[0].adserverTargeting.hb_deal_type_improve).to.equal('pg'); + }); + it('should set currency', function () { const response = deepClone(serverResponse); response.body.cur = 'EUR'; diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index e24bcb3b455..f9aa3b913c6 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -179,6 +179,113 @@ describe('InsticatorBidAdapter', function () { } })).to.be.false; }); + + it('should return false if video plcmt is not a number', () => { + expect(spec.isBidRequestValid({ + ...bidRequest, + ...{ + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + w: 250, + h: 300, + plcmt: 'NaN', + }, + } + } + })).to.be.false; + }); + + it('should return true if playerSize is present instead of w and h', () => { + expect(spec.isBidRequestValid({ + ...bidRequest, + ...{ + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + playerSize: [250, 300], + placement: 1, + }, + } + } + })).to.be.true; + }); + + it('should return true if optional video fields are valid', () => { + expect(spec.isBidRequestValid({ + ...bidRequest, + ...{ + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + playerSize: [250, 300], + placement: 1, + startdelay: 1, + skip: 1, + skipmin: 1, + skipafter: 1, + minduration: 1, + maxduration: 1, + api: [1, 2], + protocols: [2], + battr: [1, 2], + playbackmethod: [1, 2], + playbackend: 1, + delivery: [1, 2], + pos: 1, + }, + } + } + })).to.be.true; + }); + + it('should return false if optional video fields are not valid', () => { + expect(spec.isBidRequestValid({ + ...bidRequest, + ...{ + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + playerSize: [250, 300], + placement: 1, + startdelay: 'NaN', + }, + } + } + })).to.be.false; + }); + + it('should return false if video min duration > max duration', () => { + expect(spec.isBidRequestValid({ + ...bidRequest, + ...{ + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + playerSize: [250, 300], + placement: 1, + minduration: 5, + maxduration: 4, + }, + } + } + })).to.be.false; + }); }); describe('buildRequests', function () { @@ -570,4 +677,87 @@ describe('InsticatorBidAdapter', function () { expect(spec.getUserSyncs({}, [response])).to.have.length(0); }) }); + + describe('Response with video Instream', function () { + const bidRequestVid = { + method: 'POST', + url: 'https://ex.ingage.tech/v1/openrtb', + options: { + contentType: 'application/json', + withCredentials: true, + }, + data: '', + bidderRequest: { + bidderRequestId: '22edbae2733bf6', + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + timeout: 300, + bids: [ + { + bidder: 'insticator', + params: { + adUnitId: '1a2b3c4d5e6f1a2b3c4d' + }, + adUnitCode: 'adunit-code-1', + mediaTypes: { + video: { + mimes: [ + 'video/mp4', + 'video/mpeg', + ], + playerSize: [[250, 300]], + placement: 2, + plcmt: 2, + } + }, + bidId: 'bid1', + } + ] + } + }; + + const bidResponseVid = { + body: { + id: '22edbae2733bf6', + bidid: 'foo9876', + cur: 'USD', + seatbid: [ + { + seat: 'some-dsp', + bid: [ + { + ad: '', + impid: 'bid1', + crid: 'crid1', + price: 0.5, + w: 300, + h: 250, + adm: '', + exp: 60, + adomain: ['test1.com'], + ext: { + meta: { + test: 1 + } + }, + } + ], + }, + ] + } + }; + const bidRequestWithVideo = utils.deepClone(bidRequestVid); + + it('should have related properties for video Instream', function() { + const serverResponseWithInstream = utils.deepClone(bidResponseVid); + serverResponseWithInstream.body.seatbid[0].bid[0].vastXml = ''; + serverResponseWithInstream.body.seatbid[0].bid[0].mediaType = 'video'; + const bidResponse = spec.interpretResponse(serverResponseWithInstream, bidRequestWithVideo)[0]; + expect(bidResponse).to.have.any.keys('mediaType', 'vastXml', 'vastUrl'); + expect(bidResponse).to.have.property('mediaType', 'video'); + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse).to.have.property('vastXml', ''); + expect(bidResponse.vastUrl).to.match(/^data:text\/xml;charset=utf-8;base64,[\w+/=]+$/) + }); + }) }); diff --git a/test/spec/modules/integr8BidAdapter_spec.js b/test/spec/modules/integr8BidAdapter_spec.js index 8c5a4b47903..01bb706df25 100644 --- a/test/spec/modules/integr8BidAdapter_spec.js +++ b/test/spec/modules/integr8BidAdapter_spec.js @@ -72,7 +72,7 @@ describe('integr8AdapterTest', () => { }); it('bidRequest url', () => { - const endpointUrl = 'https://integr8.central.gjirafa.tech/bid'; + const endpointUrl = 'https://central.sea.integr8.digital/bid'; const requests = spec.buildRequests(bidRequests); requests.forEach(function (requestItem) { expect(requestItem.url).to.match(new RegExp(`${endpointUrl}`)); @@ -113,7 +113,7 @@ describe('integr8AdapterTest', () => { describe('interpretResponse', () => { const bidRequest = { 'method': 'POST', - 'url': 'https://integr8.central.gjirafa.tech/bid', + 'url': 'https://central.sea.integr8.digital/bid', 'data': { 'sizes': '728x90', 'adUnitId': 'hb-leaderboard', diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index 7ee6b464996..056255c7738 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -44,6 +44,78 @@ describe('invibesBidAdapter:', function () { } ]; + let bidRequestsWithDuplicatedplacementId = [ + { + bidId: 'b1', + bidder: BIDDER_CODE, + bidderRequestId: 'r1', + params: { + placementId: PLACEMENT_ID, + disableUserSyncs: false + + }, + adUnitCode: 'test-div1', + auctionId: 'a1', + sizes: [ + [300, 250], + [400, 300], + [125, 125] + ], + transactionId: 't1' + }, { + bidId: 'b2', + bidder: BIDDER_CODE, + bidderRequestId: 'r2', + params: { + placementId: PLACEMENT_ID, + disableUserSyncs: false + }, + adUnitCode: 'test-div2', + auctionId: 'a2', + sizes: [ + [300, 250], + [400, 300] + ], + transactionId: 't2' + } + ]; + + let bidRequestsWithUniquePlacementId = [ + { + bidId: 'b1', + bidder: BIDDER_CODE, + bidderRequestId: 'r1', + params: { + placementId: 'PLACEMENT_ID_1', + disableUserSyncs: false + + }, + adUnitCode: 'test-div1', + auctionId: 'a1', + sizes: [ + [300, 250], + [400, 300], + [125, 125] + ], + transactionId: 't1' + }, { + bidId: 'b2', + bidder: BIDDER_CODE, + bidderRequestId: 'r2', + params: { + placementId: 'PLACEMENT_ID_2', + disableUserSyncs: false + }, + adUnitCode: 'test-div2', + auctionId: 'a2', + sizes: [ + [300, 250], + [400, 300] + ], + transactionId: 't2' + } + ]; + let bidRequestsWithUserId = [ { bidId: 'b1', @@ -185,17 +257,44 @@ describe('invibesBidAdapter:', function () { expect(request.data.preventPageViewEvent).to.be.false; }); + it('sends isPlacementRefresh as false when the placement ids are used for the first time', function () { + let request = spec.buildRequests(bidRequestsWithUniquePlacementId, bidderRequestWithPageInfo); + expect(request.data.isPlacementRefresh).to.be.false; + }); + it('sends preventPageViewEvent as true on 2nd call', function () { let request = spec.buildRequests(bidRequests, bidderRequestWithPageInfo); expect(request.data.preventPageViewEvent).to.be.true; }); + it('sends isPlacementRefresh as true on multi requests on the same placement id', function () { + let request = spec.buildRequests(bidRequestsWithDuplicatedplacementId, bidderRequestWithPageInfo); + expect(request.data.isPlacementRefresh).to.be.true; + }); + + it('sends isInfiniteScrollPage as false initially', function () { + let request = spec.buildRequests(bidRequests, bidderRequestWithPageInfo); + expect(request.data.isInfiniteScrollPage).to.be.false; + }); + + it('sends isPlacementRefresh as true on multi requests multiple calls with the same placement id from second call', function () { + let request = spec.buildRequests(bidRequests, bidderRequestWithPageInfo); + expect(request.data.isInfiniteScrollPage).to.be.false; + let duplicatedRequest = spec.buildRequests(bidRequests, bidderRequestWithPageInfo); + expect(duplicatedRequest.data.isPlacementRefresh).to.be.true; + }); + it('sends bid request to ENDPOINT via GET', function () { const request = spec.buildRequests(bidRequests, bidderRequestWithPageInfo); expect(request.url).to.equal(ENDPOINT); expect(request.method).to.equal('GET'); }); + it('generates a visitId of length 32', function () { + spec.buildRequests(bidRequests, bidderRequestWithPageInfo); + expect(top.window.invibes.visitId.length).to.equal(32); + }); + it('sends bid request to custom endpoint via GET', function () { const request = spec.buildRequests([{ bidId: 'b1', diff --git a/test/spec/modules/iqxBidAdapter_spec.js b/test/spec/modules/iqxBidAdapter_spec.js new file mode 100644 index 00000000000..f5e680c8e0b --- /dev/null +++ b/test/spec/modules/iqxBidAdapter_spec.js @@ -0,0 +1,455 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec, getBidFloor} from 'modules/iqxBidAdapter.js'; +import {deepClone} from 'src/utils'; + +const ENDPOINT = 'https://pbjs.iqzonertb.live'; + +const defaultRequest = { + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; +describe('iqxBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'iqx', + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['iqx'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['iqx']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequestVideo}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}) diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index eee0335524d..500f2239e55 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -188,6 +188,35 @@ describe('IndexexchangeAdapter', function () { } ]; + const DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED = [ + { + bidder: 'ix', + params: { + siteId: '123', + size: [300, 250] + }, + sizes: [[300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + pos: 0 + } + }, + ortb2Imp: { + ext: { + tid: '173f49a8-7549-4218-a23c-e7ba59b47229', + ae: 1 // Fledge enabled + }, + }, + adUnitCode: 'div-fledge-ad-1460505748561-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47229', + bidId: '1a2b3c4d', + bidderRequestId: '11a22b33c44d', + auctionId: '1aa2bb3cc4dd', + schain: SAMPLE_SCHAIN + } + ]; + const DEFAULT_BANNER_VALID_BID_PARAM_NO_SIZE = [ { bidder: 'ix', @@ -735,6 +764,49 @@ describe('IndexexchangeAdapter', function () { } }; + const DEFAULT_OPTION_FLEDGE_ENABLED_GLOBALLY = { + gdprConsent: { + gdprApplies: true, + consentString: '3huaa11=qu3198ae', + vendorData: {} + }, + refererInfo: { + page: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + }, + ortb2: { + site: { + page: 'https://www.prebid.org' + }, + source: { + tid: 'mock-tid' + } + }, + fledgeEnabled: true, + defaultForSlots: 1 + }; + + const DEFAULT_OPTION_FLEDGE_ENABLED = { + gdprConsent: { + gdprApplies: true, + consentString: '3huaa11=qu3198ae', + vendorData: {} + }, + refererInfo: { + page: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + }, + ortb2: { + site: { + page: 'https://www.prebid.org' + }, + source: { + tid: 'mock-tid' + } + }, + fledgeEnabled: true + }; + const DEFAULT_IDENTITY_RESPONSE = { IdentityIp: { responsePending: false, @@ -759,7 +831,10 @@ describe('IndexexchangeAdapter', function () { // similar to uid2, but id5's getValue takes .uid id5id: { uid: 'testid5id' }, // ID5 imuid: 'testimuid', - '33acrossId': { envelope: 'v1.5fs.1000.fjdiosmclds' } + '33acrossId': { envelope: 'v1.5fs.1000.fjdiosmclds' }, + 'criteoID': { envelope: 'testcriteoID' }, + 'euidID': { envelope: 'testeuid' }, + pairId: {envelope: 'testpairId'} }; const DEFAULT_USERID_PAYLOAD = [ @@ -818,6 +893,21 @@ describe('IndexexchangeAdapter', function () { uids: [{ id: DEFAULT_USERID_DATA['33acrossId'].envelope }] + }, { + source: 'criteo.com', + uids: [{ + id: DEFAULT_USERID_DATA['criteoID'].envelope + }] + }, { + source: 'euid.eu', + uids: [{ + id: DEFAULT_USERID_DATA['euidID'].envelope + }] + }, { + source: 'google.com', + uids: [{ + id: DEFAULT_USERID_DATA['pairId'].envelope + }] } ]; @@ -1224,7 +1314,7 @@ describe('IndexexchangeAdapter', function () { const payload = extractPayload(request[0]); expect(request).to.be.an('array'); expect(request).to.have.lengthOf.above(0); // should be 1 or more - expect(payload.user.eids).to.have.lengthOf(8); + expect(payload.user.eids).to.have.lengthOf(11); expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[0]); }); }); @@ -1412,7 +1502,7 @@ describe('IndexexchangeAdapter', function () { cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = extractPayload(request); - expect(payload.user.eids).to.have.lengthOf(8); + expect(payload.user.eids).to.have.lengthOf(11); expect(payload.user.eids).to.have.deep.members(DEFAULT_USERID_PAYLOAD); }); @@ -1545,7 +1635,7 @@ describe('IndexexchangeAdapter', function () { }) expect(payload.user).to.exist; - expect(payload.user.eids).to.have.lengthOf(10); + expect(payload.user.eids).to.have.lengthOf(13); expect(payload.user.eids).to.have.deep.members(validUserIdPayload); }); @@ -1587,7 +1677,7 @@ describe('IndexexchangeAdapter', function () { }); const payload = extractPayload(request); - expect(payload.user.eids).to.have.lengthOf(9); + expect(payload.user.eids).to.have.lengthOf(12); expect(payload.user.eids).to.have.deep.members(validUserIdPayload); }); }); @@ -3140,6 +3230,149 @@ describe('IndexexchangeAdapter', function () { }); }); + describe('buildRequestFledge', function () { + it('impression should have ae=1 in ext when fledge module is enabled and ae is set in ad unit', function () { + const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED[0]); + const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; + const impression = extractPayload(requestBidFloor).imp[0]; + + expect(impression.ext.ae).to.equal(1); + }); + + it('impression should have ae=1 in ext when fledge module is enabled globally and default is set through setConfig', function () { + const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED_GLOBALLY); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; + const impression = extractPayload(requestBidFloor).imp[0]; + + expect(impression.ext.ae).to.equal(1); + }); + + it('impression should have ae=1 in ext when fledge module is enabled globally but no default set through setConfig but set at ad unit level', function () { + const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED[0]); + const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; + const impression = extractPayload(requestBidFloor).imp[0]; + + expect(impression.ext.ae).to.equal(1); + }); + + it('impression should not have ae=1 in ext when fledge module is enabled globally through setConfig but overidden at ad unit level', function () { + const bidderRequest = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; + const impression = extractPayload(requestBidFloor).imp[0]; + + expect(impression.ext.ae).to.be.undefined; + }); + + it('impression should not have ae=1 in ext when fledge module is disabled', function () { + const bidderRequest = deepClone(DEFAULT_OPTION); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const requestBidFloor = spec.buildRequests([bid], bidderRequest)[0]; + const impression = extractPayload(requestBidFloor).imp[0]; + + expect(impression.ext.ae).to.be.undefined; + }); + + it('should contain correct IXdiag ae property for Fledge', function () { + const bid = DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED[0]; + const bidderRequestWithFledgeEnabled = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); + const request = spec.buildRequests([bid], bidderRequestWithFledgeEnabled); + const diagObj = extractPayload(request[0]).ext.ixdiag; + expect(diagObj.ae).to.equal(true); + }); + + it('should log warning for non integer auction environment in ad unit for fledge', () => { + const logWarnSpy = sinon.spy(utils, 'logWarn'); + const bid = DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED[0]; + bid.ortb2Imp.ext.ae = 'malformed' + const bidderRequestWithFledgeEnabled = deepClone(DEFAULT_OPTION_FLEDGE_ENABLED); + spec.buildRequests([bid], bidderRequestWithFledgeEnabled); + expect(logWarnSpy.calledWith('error setting auction environment flag - must be an integer')).to.be.true; + logWarnSpy.restore(); + }); + }); + + describe('integration through exchangeId and externalId', function () { + const expectedExchangeId = 123456; + // create banner bids with externalId but no siteId as bidder param + const bannerBids = utils.deepClone(DEFAULT_BANNER_VALID_BID); + delete bannerBids[0].params.siteId; + bannerBids[0].params.externalId = 'exteranl_id_1'; + + beforeEach(() => { + config.setConfig({ exchangeId: expectedExchangeId }); + spec.resetSiteID(); + }); + + afterEach(() => { + config.resetConfig(); + }); + + it('when exchangeId and externalId set but no siteId, isBidRequestValid should return true', function () { + const bid = utils.deepClone(bannerBids[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('when neither exchangeId nor siteId set, isBidRequestValid should return false', function () { + config.resetConfig(); + const bid = utils.deepClone(bannerBids[0]); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('when exchangeId and externalId set with banner impression but no siteId, bidrequest sent to endpoint with p param and externalID inside imp.ext', function () { + const requests = spec.buildRequests(bannerBids, DEFAULT_OPTION); + const payload = extractPayload(requests[0]); + + const expectedURL = IX_SECURE_ENDPOINT + '?p=' + expectedExchangeId; + expect(requests[0].url).to.equal(expectedURL); + expect(payload.imp[0].ext.externalID).to.equal(bannerBids[0].params.externalId); + expect(payload.imp[0].banner.format[0].ext).to.be.undefined; + expect(payload.imp[0].ext.siteID).to.be.undefined; + }); + + it('when exchangeId and externalId set with video impression, bidrequest sent to endpoint with p param and externalID inside imp.ext', function () { + const validBids = utils.deepClone(DEFAULT_VIDEO_VALID_BID); + delete validBids[0].params.siteId; + validBids[0].params.externalId = 'exteranl_id_1'; + + const requests = spec.buildRequests(validBids, DEFAULT_OPTION); + const payload = extractPayload(requests[0]); + + const expectedURL = IX_SECURE_ENDPOINT + '?p=' + expectedExchangeId; + expect(requests[0].url).to.equal(expectedURL); + expect(payload.imp[0].ext.externalID).to.equal(validBids[0].params.externalId); + expect(payload.imp[0].ext.siteID).to.be.undefined; + }); + + it('when exchangeId and externalId set beside siteId, bidrequest sent to endpoint with both p param and s param and externalID inside imp.ext and siteID inside imp.banner.format.ext', function () { + bannerBids[0].params.siteId = '1234'; + const requests = spec.buildRequests(bannerBids, DEFAULT_OPTION); + const payload = extractPayload(requests[0]); + + const expectedURL = IX_SECURE_ENDPOINT + '?s=' + bannerBids[0].params.siteId + '&p=' + expectedExchangeId; + expect(requests[0].url).to.equal(expectedURL); + expect(payload.imp[0].ext.externalID).to.equal(bannerBids[0].params.externalId); + expect(payload.imp[0].banner.format[0].ext.externalID).to.be.undefined; + expect(payload.imp[0].ext.siteID).to.be.undefined; + expect(payload.imp[0].banner.format[0].ext.siteID).to.equal(bannerBids[0].params.siteId); + }); + + it('when exchangeId and siteId set, but no externalId, bidrequest sent to exchange', function () { + bannerBids[0].params.siteId = '1234'; + delete bannerBids[0].params.externalId; + const requests = spec.buildRequests(bannerBids, DEFAULT_OPTION); + const payload = extractPayload(requests[0]); + + const expectedURL = IX_SECURE_ENDPOINT + '?s=' + bannerBids[0].params.siteId + '&p=' + expectedExchangeId; + expect(requests[0].url).to.equal(expectedURL); + expect(payload.imp[0].ext.externalID).to.be.undefined; + expect(payload.imp[0].banner.format[0].ext.siteID).to.equal(bannerBids[0].params.siteId); + }); + }); + describe('interpretResponse', function () { // generate bidderRequest with real buildRequest logic for intepretResponse testing let bannerBidderRequest @@ -3663,6 +3896,140 @@ describe('IndexexchangeAdapter', function () { const result = spec.interpretResponse({ body: DEFAULT_NATIVE_BID_RESPONSE }, nativeBidderRequest); expect(result[0]).to.deep.equal(expectedParse[0]); }); + + describe('Auction config response', function () { + let bidderRequestWithFledgeEnabled; + let serverResponseWithoutFledgeConfigs; + let serverResponseWithFledgeConfigs; + let serverResponseWithMalformedAuctionConfig; + let serverResponseWithMalformedAuctionConfigs; + + beforeEach(() => { + bidderRequestWithFledgeEnabled = spec.buildRequests(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED, {})[0]; + bidderRequestWithFledgeEnabled.fledgeEnabled = true; + + serverResponseWithoutFledgeConfigs = { + body: { + ...DEFAULT_BANNER_BID_RESPONSE + } + }; + + serverResponseWithFledgeConfigs = { + body: { + ...DEFAULT_BANNER_BID_RESPONSE, + ext: { + protectedAudienceAuctionConfigs: [ + { + bidId: '59f219e54dc2fc', + config: { + seller: 'https://seller.test.indexexchange.com', + decisionLogicUrl: 'https://seller.test.indexexchange.com/decision-logic.js', + interestGroupBuyers: ['https://buyer.test.indexexchange.com'], + sellerSignals: { + callbackURL: 'https://test.com/ig/v1/ck74j8bcvc9c73a8eg6g' + }, + perBuyerSignals: { + 'https://buyer.test.indexexchange.com': {} + } + } + } + ] + } + } + }; + + serverResponseWithMalformedAuctionConfig = { + body: { + ...DEFAULT_BANNER_BID_RESPONSE, + ext: { + protectedAudienceAuctionConfigs: ['malformed'] + } + } + }; + + serverResponseWithMalformedAuctionConfigs = { + body: { + ...DEFAULT_BANNER_BID_RESPONSE, + ext: { + protectedAudienceAuctionConfigs: 'malformed' + } + } + }; + }); + + it('should correctly interpret response with auction configs', () => { + const result = spec.interpretResponse(serverResponseWithFledgeConfigs, bidderRequestWithFledgeEnabled); + const expectedOutput = [ + { + bidId: '59f219e54dc2fc', + config: { + ...serverResponseWithFledgeConfigs.body.ext.protectedAudienceAuctionConfigs[0].config, + perBuyerSignals: { + 'https://buyer.test.indexexchange.com': {} + } + } + } + ]; + expect(result.fledgeAuctionConfigs).to.deep.equal(expectedOutput); + }); + + it('should correctly interpret response without auction configs', () => { + const result = spec.interpretResponse(serverResponseWithoutFledgeConfigs, bidderRequestWithFledgeEnabled); + expect(result.fledgeAuctionConfigs).to.be.undefined; + }); + + it('should handle malformed auction configs gracefully', () => { + const result = spec.interpretResponse(serverResponseWithMalformedAuctionConfig, bidderRequestWithFledgeEnabled); + expect(result.fledgeAuctionConfigs).to.be.empty; + }); + + it('should log warning for malformed auction configs', () => { + const logWarnSpy = sinon.spy(utils, 'logWarn'); + spec.interpretResponse(serverResponseWithMalformedAuctionConfig, bidderRequestWithFledgeEnabled); + expect(logWarnSpy.calledWith('Malformed auction config detected:', 'malformed')).to.be.true; + logWarnSpy.restore(); + }); + + it('should return bids when protected audience auction conigs is malformed', () => { + const result = spec.interpretResponse(serverResponseWithMalformedAuctionConfigs, bidderRequestWithFledgeEnabled); + expect(result.fledgeAuctionConfigs).to.be.undefined; + expect(result.length).to.be.greaterThan(0); + }); + }); + + describe('interpretResponse when server response is empty', function() { + let serverResponseWithoutBody; + let serverResponseWithoutSeatbid; + let bidderRequestWithFledgeEnabled; + let bidderRequestWithoutFledgeEnabled; + + beforeEach(() => { + serverResponseWithoutBody = {}; + + serverResponseWithoutSeatbid = { + body: {} + }; + + bidderRequestWithFledgeEnabled = spec.buildRequests(DEFAULT_BANNER_VALID_BID_WITH_FLEDGE_ENABLED, {})[0]; + bidderRequestWithFledgeEnabled.fledgeEnabled = true; + + bidderRequestWithoutFledgeEnabled = spec.buildRequests(DEFAULT_BANNER_VALID_BID, {})[0]; + }); + + it('should return empty bids when response does not have body', function () { + let result = spec.interpretResponse(serverResponseWithoutBody, bidderRequestWithFledgeEnabled); + expect(result).to.deep.equal([]); + result = spec.interpretResponse(serverResponseWithoutBody, bidderRequestWithoutFledgeEnabled); + expect(result).to.deep.equal([]); + }); + + it('should return empty bids when response body does not have seatbid', function () { + let result = spec.interpretResponse(serverResponseWithoutSeatbid, bidderRequestWithFledgeEnabled); + expect(result).to.deep.equal([]); + result = spec.interpretResponse(serverResponseWithoutSeatbid, bidderRequestWithoutFledgeEnabled); + expect(result).to.deep.equal([]); + }); + }); }); describe('bidrequest consent', function () { diff --git a/test/spec/modules/jixieBidAdapter_spec.js b/test/spec/modules/jixieBidAdapter_spec.js index 4a0fa3b4d57..fa7618814f8 100644 --- a/test/spec/modules/jixieBidAdapter_spec.js +++ b/test/spec/modules/jixieBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec, internal as jixieaux, storage } from 'modules/jixieBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { deepClone } from 'src/utils.js'; describe('jixie Adapter', function () { const pageurl_ = 'https://testdomain.com/testpage.html'; @@ -74,13 +75,16 @@ describe('jixie Adapter', function () { const jxtokoTest1_ = 'eyJJRCI6ImFiYyJ9'; const jxifoTest1_ = 'fffffbbbbbcccccaaaaae890606aaaaa'; const jxtdidTest1_ = '222223d1-1111-2222-3333-b9f129299999'; - const __uid2_advertising_token_Test1 = 'AAAAABBBBBCCCCCDDDDDEEEEEUkkZPQfifpkPnnlJhtsa4o+gf4nfqgN5qHiTVX73ymTSbLT9jz1nf+Q7QdxNh9nTad9UaN5pzfHMt/rs1woQw72c1ip+8heZXPfKGZtZP7ldJesYhlo3/0FVcL/wl9ZlAo1jYOEfHo7Y9zFzNXABbbbbb=='; - + const jxcompTest1_ = 'AAAAABBBBBCCCCCDDDDDEEEEEUkkZPQfifpkPnnlJhtsa4o+gf4nfqgN5qHiTVX73ymTSbLT9jz1nf+Q7QdxNh9nTad9UaN5pzfHMt/rs1woQw72c1ip+8heZXPfKGZtZP7ldJesYhlo3/0FVcL/wl9ZlAo1jYOEfHo7Y9zFzNXABbbbbb=='; + const ckname1Val_ = 'ckckname1'; + const ckname2Val_ = 'ckckname2'; const refJxEids_ = { + 'pubid1': ckname1Val_, + 'pubid2': ckname2Val_, '_jxtoko': jxtokoTest1_, '_jxifo': jxifoTest1_, '_jxtdid': jxtdidTest1_, - '__uid2_advertising_token': __uid2_advertising_token_Test1 + '_jxcomp': jxcompTest1_ }; // to serve as the object that prebid will call jixie buildRequest with: (param2) @@ -205,6 +209,17 @@ describe('jixie Adapter', function () { } ]; + const testJixieCfg_ = { + genids: [ + { id: 'pubid1', ck: 'ckname1' }, + { id: 'pubid2', ck: 'ckname2' }, + { id: '_jxtoko' }, + { id: '_jxifo' }, + { id: '_jxtdid' }, + { id: '_jxcomp' } + ] + }; + it('should attach valid params to the adserver endpoint (1)', function () { // this one we do not intercept the cookie stuff so really don't know // what will be in there. so we do not check here (using expect) @@ -215,7 +230,6 @@ describe('jixie Adapter', function () { }) expect(request.data).to.be.an('string'); const payload = JSON.parse(request.data); - expect(payload).to.have.property('auctionid', auctionId_); expect(payload).to.have.property('timeout', timeout_); expect(payload).to.have.property('currency', currency_); expect(payload).to.have.property('bids').that.deep.equals(refBids_); @@ -225,8 +239,25 @@ describe('jixie Adapter', function () { // similar to above test case but here we force some clientid sessionid values // and domain, pageurl // get the interceptors ready: + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.callsFake(function fakeFn(prop) { + if (prop == 'jixie') { + return testJixieCfg_; + } + return null; + }); + let getCookieStub = sinon.stub(storage, 'getCookie'); let getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getCookieStub + .withArgs('ckname1') + .returns(ckname1Val_); + getCookieStub + .withArgs('ckname2') + .returns(ckname2Val_); + getCookieStub + .withArgs('_jxtoko') + .returns(jxtokoTest1_); getCookieStub .withArgs('_jxtoko') .returns(jxtokoTest1_); @@ -237,8 +268,8 @@ describe('jixie Adapter', function () { .withArgs('_jxtdid') .returns(jxtdidTest1_); getCookieStub - .withArgs('__uid2_advertising_token') - .returns(__uid2_advertising_token_Test1); + .withArgs('_jxcomp') + .returns(jxcompTest1_); getCookieStub .withArgs('_jxx') .returns(clientIdTest1_); @@ -264,7 +295,6 @@ describe('jixie Adapter', function () { expect(request.data).to.be.an('string'); const payload = JSON.parse(request.data); - expect(payload).to.have.property('auctionid', auctionId_); expect(payload).to.have.property('client_id_c', clientIdTest1_); expect(payload).to.have.property('client_id_ls', clientIdTest1_); expect(payload).to.have.property('session_id_c', sessionIdTest1_); @@ -281,6 +311,7 @@ describe('jixie Adapter', function () { // unwire interceptors getCookieStub.restore(); getLocalStorageStub.restore(); + getConfigStub.restore(); miscDimsStub.restore(); });// it @@ -345,6 +376,43 @@ describe('jixie Adapter', function () { expect(payload.schain).to.deep.include(schain); }); + it('it should populate the floor info when available', function () { + let oneSpecialBidReq = deepClone(bidRequests_[0]); + let request, payload = null; + // 1 floor is not set + request = spec.buildRequests([oneSpecialBidReq], bidderRequest_); + payload = JSON.parse(request.data); + expect(payload.bids[0].bidFloor).to.not.exist; + + // 2 floor is set + let getFloorResponse = { currency: 'USD', floor: 2.1 }; + oneSpecialBidReq.getFloor = () => getFloorResponse; + request = spec.buildRequests([oneSpecialBidReq], bidderRequest_); + payload = JSON.parse(request.data); + expect(payload.bids[0].bidFloor).to.exist.and.to.equal(2.1); + }); + + it('it should populate the aid field when available', function () { + let oneSpecialBidReq = deepClone(bidRequests_[0]); + // 1 aid is not set in the jixie config + let request = spec.buildRequests([oneSpecialBidReq], bidderRequest_); + let payload = JSON.parse(request.data); + expect(payload.aid).to.eql(''); + + // 2 aid is set in the jixie config + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.callsFake(function fakeFn(prop) { + if (prop == 'jixie') { + return { aid: '11223344556677889900' }; + } + return null; + }); + request = spec.buildRequests([oneSpecialBidReq], bidderRequest_); + payload = JSON.parse(request.data); + expect(payload.aid).to.exist.and.to.equal('11223344556677889900'); + getConfigStub.restore(); + }); + it('should populate eids when supported userIds are available', function () { const oneSpecialBidReq = Object.assign({}, bidRequests_[0], { userIdAsEids: [ @@ -408,7 +476,6 @@ describe('jixie Adapter', function () { 'bids': [ // video (vast tag url) returned here { - 'trackingUrlBase': 'https://traid.jixie.io/sync/ad?', 'jxBidId': '62847e4c696edcb-028d5dee-2c83-44e3-bed1-b75002475cdf', 'requestId': '62847e4c696edcb', 'cpm': 2.19, @@ -441,7 +508,6 @@ describe('jixie Adapter', function () { // display ad returned here: This one there is advertiserDomains // in the response . Will be checked in the unit tests below { - 'trackingUrlBase': 'https://traid.jixie.io/sync/ad?', 'jxBidId': '600c9ae6fda1acb-028d5dee-2c83-44e3-bed1-b75002475cdf', 'requestId': '600c9ae6fda1acb', 'cpm': 1.999, @@ -478,7 +544,6 @@ describe('jixie Adapter', function () { }, // outstream, jx non-default renderer specified: { - 'trackingUrlBase': 'https://traid.jixie.io/sync/ad?', 'jxBidId': '99bc539c81b00ce-028d5dee-2c83-44e3-bed1-b75002475cdf', 'requestId': '99bc539c81b00ce', 'cpm': 2.99, @@ -497,7 +562,6 @@ describe('jixie Adapter', function () { }, // outstream, jx default renderer: { - 'trackingUrlBase': 'https://traid.jixie.io/sync/ad?', 'jxBidId': '61bc539c81b00ce-028d5dee-2c83-44e3-bed1-b75002475cdf', 'requestId': '61bc539c81b00ce', 'cpm': 1.99, @@ -568,7 +632,6 @@ describe('jixie Adapter', function () { expect(result[0].netRevenue).to.equal(true) expect(result[0].ttl).to.equal(300) expect(result[0].vastUrl).to.include('https://ad.jixie.io/v1/video?creativeid=') - expect(result[0].trackingUrlBase).to.include('sync') // We will always make sure the meta->advertiserDomains property is there // If no info it is an empty array. expect(result[0].meta.advertiserDomains.length).to.equal(0) @@ -584,7 +647,6 @@ describe('jixie Adapter', function () { expect(result[1].ttl).to.equal(300) expect(result[1].ad).to.include('jxoutstream') expect(result[1].meta.advertiserDomains.length).to.equal(3) - expect(result[1].trackingUrlBase).to.include('sync') // should pick up about using alternative outstream renderer expect(result[2].requestId).to.equal('99bc539c81b00ce') @@ -596,7 +658,6 @@ describe('jixie Adapter', function () { expect(result[2].netRevenue).to.equal(true) expect(result[2].ttl).to.equal(300) expect(result[2].vastXml).to.include('') - expect(result[2].trackingUrlBase).to.include('sync'); expect(result[2].renderer.id).to.equal('demoslot4-div') expect(result[2].meta.advertiserDomains.length).to.equal(0) expect(result[2].renderer.url).to.equal(JX_OTHER_OUTSTREAM_RENDERER_URL); @@ -611,7 +672,6 @@ describe('jixie Adapter', function () { expect(result[3].netRevenue).to.equal(true) expect(result[3].ttl).to.equal(300) expect(result[3].vastXml).to.include('') - expect(result[3].trackingUrlBase).to.include('sync'); expect(result[3].renderer.id).to.equal('demoslot2-div') expect(result[3].meta.advertiserDomains.length).to.equal(0) expect(result[3].renderer.url).to.equal(JX_OUTSTREAM_RENDERER_URL) @@ -646,116 +706,5 @@ describe('jixie Adapter', function () { spec.onBidWon({ trackingUrl: TRACKINGURL_ }) expect(jixieaux.ajax.calledWith(TRACKINGURL_)).to.equal(true); }) - - it('Should not fire if the adserver response indicates no firing', function() { - let called = false; - ajaxStub.callsFake(function fakeFn() { - called = true; - }); - spec.onBidWon({ notrack: 1 }) - expect(called).to.equal(false); - }); - - // A reference to check again: - const QPARAMS_ = { - action: 'hbbidwon', - device: device_, - pageurl: encodeURIComponent(pageurl_), - domain: encodeURIComponent(domain_), - cid: 121, - cpid: 99, - jxbidid: '62847e4c696edcb-028d5dee-2c83-44e3-bed1-b75002475cdf', - auctionid: '028d5dee-2c83-44e3-bed1-b75002475cdf', - cpm: 1.11, - requestid: '62847e4c696edcb' - }; - - it('check it is sending the correct ajax url and qparameters', function() { - spec.onBidWon({ - trackingUrlBase: 'https://mytracker.com/sync?', - cid: 121, - cpid: 99, - jxBidId: '62847e4c696edcb-028d5dee-2c83-44e3-bed1-b75002475cdf', - auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', - cpm: 1.11, - requestId: '62847e4c696edcb' - }) - expect(jixieaux.ajax.calledWith('https://mytracker.com/sync?', null, QPARAMS_)).to.equal(true); - }); }); // describe - - /** - * onTimeout - */ - describe('onTimeout', function() { - let ajaxStub; - let miscDimsStub; - beforeEach(function() { - ajaxStub = sinon.stub(jixieaux, 'ajax'); - miscDimsStub = sinon.stub(jixieaux, 'getMiscDims'); - miscDimsStub - .returns({ device: device_, pageurl: pageurl_, domain: domain_, mkeywords: keywords_ }); - }) - - afterEach(function() { - miscDimsStub.restore(); - ajaxStub.restore(); - }) - - // reference to check against: - const QPARAMS_ = { - action: 'hbtimeout', - device: device_, - pageurl: encodeURIComponent(pageurl_), - domain: encodeURIComponent(domain_), - auctionid: '028d5dee-2c83-44e3-bed1-b75002475cdf', - timeout: 1000, - count: 2 - }; - - it('check it is sending the correct ajax url and qparameters', function() { - spec.onTimeout([ - {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000}, - {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000} - ]) - expect(jixieaux.ajax.calledWith(spec.EVENTS_URL, null, QPARAMS_)).to.equal(true); - }) - - it('if turned off via config then dont do onTimeout sending of event', function() { - let getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.callsFake(function fakeFn(prop) { - if (prop == 'jixie') { - return { onTimeout: 'off' }; - } - return null; - }); - let called = false; - ajaxStub.callsFake(function fakeFn() { - called = true; - }); - spec.onTimeout([ - {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000}, - {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000} - ]) - expect(called).to.equal(false); - getConfigStub.restore(); - }) - - const otherUrl_ = 'https://other.azurewebsites.net/sync/evt?'; - it('if config specifies a different endpoint then should send there instead', function() { - let getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.callsFake(function fakeFn(prop) { - if (prop == 'jixie') { - return { onTimeoutUrl: otherUrl_ }; - } - return null; - }); - spec.onTimeout([ - {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000}, - {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000} - ]) - expect(jixieaux.ajax.calledWith(otherUrl_, null, QPARAMS_)).to.equal(true); - getConfigStub.restore(); - }) - });// describe }); diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index 9f7a4854063..f43c3b11aac 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -142,6 +142,27 @@ describe('kargo adapter tests', function () { 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' + }, + ] + }, + ] } }, ortb2Imp: { @@ -150,9 +171,9 @@ describe('kargo adapter tests', function () { data: { adServer: { name: 'gam', - adSlot: '/22558409563,18834096/dfy_mobile_adhesion' + adslot: '/22558409563,18834096/dfy_mobile_adhesion' }, - pbAdSlot: '/22558409563,18834096/dfy_mobile_adhesion' + pbadslot: '/22558409563,18834096/dfy_mobile_adhesion' }, gpid: '/22558409563,18834096/dfy_mobile_adhesion' } @@ -179,9 +200,9 @@ describe('kargo adapter tests', function () { data: { adServer: { name: 'gam', - adSlot: '/22558409563,18834096/dfy_mobile_adhesion' + adslot: '/22558409563,18834096/dfy_mobile_adhesion' }, - pbAdSlot: '/22558409563,18834096/dfy_mobile_adhesion' + pbadslot: '/22558409563,18834096/dfy_mobile_adhesion' } } } @@ -204,9 +225,10 @@ describe('kargo adapter tests', function () { data: { adServer: { name: 'gam', - adSlot: '/22558409563,18834096/dfy_mobile_adhesion' + adslot: '/22558409563,18834096/dfy_mobile_adhesion' } - } + }, + gpid: '/22558409563,18834096/dfy_mobile_adhesion' } } } @@ -439,6 +461,9 @@ describe('kargo adapter tests', function () { source: 1 }, }, + site: { + cat: ['IAB1', 'IAB2', 'IAB3'] + }, imp: [ { code: '101', @@ -513,6 +538,20 @@ describe('kargo adapter tests', function () { } ] } + ], + data: [ + { + name: 'prebid.org', + ext: { + segtax: 600, + segclass: 'v1', + }, + segment: [ + { + id: '133' + } + ] + } ] } }; diff --git a/test/spec/modules/kulturemediaBidAdapter_spec.js b/test/spec/modules/kulturemediaBidAdapter_spec.js deleted file mode 100644 index f21fe4a8810..00000000000 --- a/test/spec/modules/kulturemediaBidAdapter_spec.js +++ /dev/null @@ -1,612 +0,0 @@ -import {expect} from 'chai'; -import {spec} from 'modules/kulturemediaBidAdapter.js'; - -const BANNER_REQUEST = { - 'bidderCode': 'kulturemedia', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708', - 'bidderRequestId': 'requestId', - 'bidRequest': [{ - 'bidder': 'kulturemedia', - 'params': { - 'placementId': 123456, - }, - 'placementCode': 'div-gpt-dummy-placement-code', - 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, - 'bidId': 'bidId1', - 'bidderRequestId': 'bidderRequestId', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' - }, - { - 'bidder': 'kulturemedia', - 'params': { - 'placementId': 123456, - }, - 'placementCode': 'div-gpt-dummy-placement-code', - 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, - 'bidId': 'bidId2', - 'bidderRequestId': 'bidderRequestId', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' - }], - 'start': 1487883186070, - 'auctionStart': 1487883186069, - 'timeout': 3000 -}; - -const RESPONSE = { - 'headers': null, - 'body': { - 'id': 'responseId', - 'seatbid': [ - { - 'bid': [ - { - 'id': 'bidId1', - 'impid': 'bidId1', - 'price': 0.18, - 'adm': '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 334553, - 'auction_id': 514667951122925701, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - }, - { - 'id': 'bidId2', - 'impid': 'bidId2', - 'price': 0.1, - 'adm': '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 386046, - 'auction_id': 517067951122925501, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'kulturemedia' - } - ], - 'ext': { - 'usersync': { - 'sovrn': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlsovrn', - 'type': 'iframe' - } - ] - }, - 'appnexus': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlappnexus', - 'type': 'pixel' - } - ] - } - }, - 'responsetimemillis': { - 'appnexus': 127 - } - } - } -}; - -const DEFAULT_NETWORK_ID = 1; - -describe('kulturemediaBidAdapter:', function () { - let videoBidRequest; - - const VIDEO_REQUEST = { - 'bidderCode': 'kulturemedia', - 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', - 'bidderRequestId': '34feaad34lkj2', - 'bids': videoBidRequest, - 'auctionStart': 1520001292880, - 'timeout': 3000, - 'start': 1520001292884, - 'doneCbCallCount': 0, - 'refererInfo': { - 'numIframes': 1, - 'reachedTop': true, - 'referer': 'test.com' - } - }; - - beforeEach(function () { - videoBidRequest = { - mediaTypes: { - video: { - context: 'instream', - playerSize: [[640, 480]], - } - }, - bidder: 'kulturemedia', - sizes: [640, 480], - bidId: '30b3efwfwe1e', - adUnitCode: 'video1', - params: { - video: { - playerWidth: 640, - playerHeight: 480, - mimes: ['video/mp4', 'application/javascript'], - protocols: [2, 5], - api: [2], - position: 1, - delivery: [2], - sid: 134, - rewarded: 1, - placement: 1, - hp: 1, - inventoryid: 123 - }, - site: { - id: 1, - page: 'https://test.com', - referrer: 'http://test.com' - }, - publisherId: 'km123' - } - }; - }); - - describe('isBidRequestValid', function () { - context('basic validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should accept request if placementId and publisherId are passed', function () { - expect(spec.isBidRequestValid(this.bid)).to.be.true; - }); - - it('reject requests without params', function () { - this.bid.params = {}; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when banner mediaType does not exist', function () { - this.bid.mediaTypes = {} - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - context('banner validation', function () { - it('returns true when banner sizes are defined', function () { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.true; - }); - - it('returns false when banner sizes are invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((sizes) => { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); - }); - - context('video validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - video: { - playerSize: [[300, 50]], - context: 'instream', - mimes: ['foo', 'bar'], - protocols: [1, 2] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should return true (skip validations) when e2etest = true', function () { - this.bid.params = { - e2etest: true - }; - expect(spec.isBidRequestValid(this.bid)).to.equal(true); - }); - - it('returns false when video context is not defined', function () { - delete this.bid.mediaTypes.video.context; - - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when video playserSize is invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((playerSize) => { - this.bid.mediaTypes.video.playerSize = playerSize; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - it('returns false when video mimes is invalid', function () { - const invalidMimes = [ - undefined, - 'test', - 1, - [] - ] - - invalidMimes.forEach((mimes) => { - this.bid.mediaTypes.video.mimes = mimes; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - - it('returns false when video protocols is invalid', function () { - const invalidMimes = [ - undefined, - 'test', - 1, - [] - ] - - invalidMimes.forEach((protocols) => { - this.bid.mediaTypes.video.protocols = protocols; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - }); - }); - - describe('buildRequests', function () { - context('when mediaType is banner', function () { - it('creates request data', function () { - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); - - expect(request).to.exist.and.to.be.a('object'); - const payload = JSON.parse(request.data); - expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId); - expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId); - }); - - it('has gdpr data if applicable', function () { - const req = Object.assign({}, BANNER_REQUEST, { - gdprConsent: { - consentString: 'consentString', - gdprApplies: true, - } - }); - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); - expect(payload.regs.ext).to.have.property('gdpr', 1); - }); - - it('should properly forward eids parameters', function () { - const req = Object.assign({}, BANNER_REQUEST); - req.bidRequest[0].userIdAsEids = [ - { - source: 'dummy.com', - uids: [ - { - id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', - atype: 1 - } - ] - }]; - let request = spec.buildRequests(req.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); - expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); - expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); - }); - }); - - context('when mediaType is video', function () { - it('should create a POST request for every bid', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach request data', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - const [width, height] = videoBidRequest.sizes; - const VERSION = '1.0.0'; - expect(data.imp[0].video.w).to.equal(width); - expect(data.imp[0].video.h).to.equal(height); - expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor); - expect(data.ext.prebidver).to.equal('$prebid.version$'); - expect(data.ext.adapterver).to.equal(spec.VERSION); - }); - - it('should set pubId to e2etest when bid.params.e2etest = true', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach End 2 End test data', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - expect(data.imp[0].bidfloor).to.not.exist; - expect(data.imp[0].video.w).to.equal(640); - expect(data.imp[0].video.h).to.equal(480); - }); - }); - }); - - describe('interpretResponse', function () { - context('when mediaType is banner', function () { - it('have bids', function () { - let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); - expect(bids).to.be.an('array').that.is.not.empty; - validateBidOnIndex(0); - validateBidOnIndex(1); - - function validateBidOnIndex(index) { - expect(bids[index]).to.have.property('currency', 'USD'); - expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); - expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); - expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); - expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); - expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); - expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); - expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain); - expect(bids[index]).to.have.property('ttl', 300); - expect(bids[index]).to.have.property('netRevenue', true); - } - }); - - it('handles empty response', function () { - const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); - const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); - - expect(bids).to.be.empty; - }); - }); - - context('when mediaType is video', function () { - it('should return no bids if the response is not valid', function () { - const bidResponse = spec.interpretResponse({ - body: null - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "nurl" and "adm" are missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - price: 6.01 - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "price" is missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - adm: '' - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return a valid video bid response with just "adm"', function () { - const serverResponse = { - id: '123', - seatbid: [{ - bid: [{ - id: 1, - adid: 123, - impid: 456, - crid: 2, - price: 6.01, - adm: '', - adomain: [ - 'kulturemedia.com' - ], - w: 640, - h: 480, - ext: { - prebid: { - type: 'video' - }, - } - }] - }], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - let o = { - requestId: serverResponse.seatbid[0].bid[0].impid, - ad: '', - cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: serverResponse.seatbid[0].bid[0].crid, - vastXml: serverResponse.seatbid[0].bid[0].adm, - width: 640, - height: 480, - mediaType: 'video', - currency: 'USD', - ttl: 300, - netRevenue: true, - meta: { - advertiserDomains: ['kulturemedia.com'] - } - }; - expect(bidResponse[0]).to.deep.equal(o); - }); - - it('should default ttl to 300', function () { - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl above 3601, default to 300', function () { - videoBidRequest.params.video.ttl = 3601; - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl below 1, default to 300', function () { - videoBidRequest.params.video.ttl = 0; - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - }); - }); - - describe('getUserSyncs', function () { - it('handles no parameters', function () { - let opts = spec.getUserSyncs({}); - expect(opts).to.be.an('array').that.is.empty; - }); - it('returns non if sync is not allowed', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); - - expect(opts).to.be.an('array').that.is.empty; - }); - - it('iframe sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('iframe'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); - }); - - it('pixel sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); - }); - - it('all sync enabled should return all results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(2); - }); - }); -}) -; diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js index 0929a022937..ad21d7b6763 100644 --- a/test/spec/modules/liveIntentIdMinimalSystem_spec.js +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -73,7 +73,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the Identity Exchange endpoint with the privided distributorId', function() { + it('should call the Identity Exchange endpoint with the provided distributorId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; @@ -87,7 +87,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint without the privided distributorId when appId is provided', function() { + it('should call the Identity Exchange endpoint without the provided distributorId when appId is provided', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; @@ -261,6 +261,11 @@ describe('LiveIntentMinimalId', function() { 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() { + 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() { 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'}}}); @@ -271,6 +276,16 @@ describe('LiveIntentMinimalId', function() { 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 () { + 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() { + 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 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 4f11af57711..3af598c5d4e 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,6 +1,6 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') @@ -12,6 +12,7 @@ describe('LiveIntentId', function() { let logErrorStub; let uspConsentDataStub; let gdprConsentDataStub; + let gppConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; @@ -24,6 +25,7 @@ describe('LiveIntentId', function() { logErrorStub = sinon.stub(utils, 'logError'); uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); }); afterEach(function() { @@ -33,6 +35,7 @@ describe('LiveIntentId', function() { logErrorStub.restore(); uspConsentDataStub.restore(); gdprConsentDataStub.restore(); + gppConsentDataStub.restore(); resetLiveIntentIdSubmodule(); }); @@ -42,11 +45,15 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1, 2] + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*/); + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1%2C2.*/); const response = { unifiedId: 'a_unified_id', segments: [123, 234] @@ -65,9 +72,13 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.getId(defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -83,6 +94,16 @@ describe('LiveIntentId', function() { }, 200); }); + it('should initialize LiveConnect and forward the prebid version when decode and emit an event', function(done) { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams + }}); + setTimeout(() => { + expect(server.requests[0].url).to.contain('tv=$prebid.version$') + done(); + }, 200); + }); + it('should initialize LiveConnect with the config params when decode and emit an event', function (done) { liveIntentIdSubmodule.decode({}, { params: { ...defaultConfigParams.params, @@ -123,9 +144,13 @@ describe('LiveIntentId', function() { gdprApplies: false, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -171,7 +196,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -179,13 +204,13 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint with the privided distributorId', function() { + it('should call the Identity Exchange endpoint with the provided distributorId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -193,13 +218,13 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint without the privided distributorId when appId is provided', function() { + it('should call the Identity Exchange endpoint without the provided distributorId when appId is provided', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -219,7 +244,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?cd=.localhost&resolve=nonId'); request.respond( 200, responseHeader, @@ -234,7 +259,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 200, responseHeader, @@ -249,7 +274,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 503, responseHeader, @@ -266,7 +291,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&resolve=nonId`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&resolve=nonId`); request.respond( 200, responseHeader, @@ -289,7 +314,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc&resolve=nonId`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&_thirdPC=third-pc&resolve=nonId`); request.respond( 200, responseHeader, @@ -311,7 +336,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); request.respond( 200, responseHeader, @@ -344,7 +369,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=nonId&resolve=foo`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId&resolve=foo`); request.respond( 200, responseHeader, @@ -353,7 +378,7 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should decode a uid2 to a seperate object when present', function() { + it('should decode a uid2 to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); @@ -363,26 +388,41 @@ describe('LiveIntentId', function() { expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a bidswitch id to a seperate object when present', function() { + it('should decode a bidswitch id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a medianet id to a seperate object when present', function() { + it('should decode a medianet id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a magnite id to a seperate object when present', function() { + it('should decode a sovrn id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode an index id to a 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 separate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + it('should allow disabling nonId resolution', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { @@ -391,7 +431,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=uid2`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=uid2`); request.respond( 200, responseHeader, diff --git a/test/spec/modules/lm_kiviadsBidAdapter_spec.js b/test/spec/modules/lm_kiviadsBidAdapter_spec.js new file mode 100644 index 00000000000..68ac73289cd --- /dev/null +++ b/test/spec/modules/lm_kiviadsBidAdapter_spec.js @@ -0,0 +1,455 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec, getBidFloor} from 'modules/lm_kiviadsBidAdapter.js'; +import {deepClone} from 'src/utils'; + +const ENDPOINT = 'https://pbjs.kiviads.live'; + +const defaultRequest = { + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; +describe('lm_kiviadsBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'lm_kiviads', + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['lm_kiviads'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['lm_kiviads']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequestVideo}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}) diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 304ce2ed7a5..0864a976d7d 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -1492,6 +1492,40 @@ describe('magnite analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + describe('when eventDispatcher is present', () => { + beforeEach(() => { + window.pbjs = window.pbjs || {}; + pbjs.rp = pbjs.rp || {}; + pbjs.rp.eventDispatcher = pbjs.rp.eventDispatcher || document.createElement('fakeElem'); + }); + + afterEach(() => { + delete pbjs.rp.eventDispatcher; + delete pbjs.rp; + }); + + it('should dispatch beforeSendingMagniteAnalytics if possible', () => { + pbjs.rp.eventDispatcher.addEventListener('beforeSendingMagniteAnalytics', (data) => { + data.detail.test = 'testData'; + }); + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('http://localhost:9999/event'); + + let message = JSON.parse(request.requestBody); + + const AnalyticsMessageWithCustomData = { + ...ANALYTICS_MESSAGE, + test: 'testData' + } + expect(message).to.deep.equal(AnalyticsMessageWithCustomData); + }); + }) + describe('when handling bid caching', () => { let auctionInits, bidRequests, bidResponses, bidsWon; beforeEach(function () { diff --git a/test/spec/modules/mediabramaBidAdapter_spec.js b/test/spec/modules/mediabramaBidAdapter_spec.js new file mode 100644 index 00000000000..d7341e02f17 --- /dev/null +++ b/test/spec/modules/mediabramaBidAdapter_spec.js @@ -0,0 +1,256 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mediabramaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; +import * as utils from '../../../src/utils.js'; + +describe('MediaBramaBidAdapter', function () { + const bid = { + bidId: '23dc19818e5293', + bidder: 'mediabrama', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 24428, + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.mediabrama.com/pbjs'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'adFormat', 'sizes', 'schain', 'bidfloor'); + expect(placement.placementId).to.equal(24428); + expect(placement.bidId).to.equal('23dc19818e5293'); + expect(placement.adFormat).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23dc19818e5293'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('should do nothing on getUserSyncs', function () { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://prebid.mediabrama.com/sync/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + }); + + describe('on bidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('should replace nurl for banner', function () { + const nurl = 'nurl/?ap=${' + 'AUCTION_PRICE}'; + const bid = { + 'bidderCode': 'mediabrama', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '5691dd18ba6ab6', + 'requestId': '23dc19818e5293', + 'transactionId': '948c716b-bf64-4303-bcf4-395c2f6a9770', + 'auctionId': 'a6b7c61f-15a9-481b-8f64-e859787e9c07', + 'mediaType': 'banner', + 'source': 'client', + 'ad': "
\n", + 'cpm': 0.61, + 'nurl': nurl, + 'creativeId': 'test', + 'currency': 'USD', + 'dealId': '', + 'meta': { + 'advertiserDomains': [], + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'name': 'mediabrama' + } + ] + } + }, + 'netRevenue': true, + 'ttl': 185, + 'metrics': {}, + 'adapterCode': 'mediabrama', + 'originalCpm': 0.61, + 'originalCurrency': 'USD', + 'responseTimestamp': 1668162732297, + 'requestTimestamp': 1668162732292, + 'bidder': 'mediabrama', + 'adUnitCode': 'div-prebid', + 'timeToRespond': 5, + 'pbLg': '0.50', + 'pbMg': '0.60', + 'pbHg': '0.61', + 'pbAg': '0.61', + 'pbDg': '0.61', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'mediabrama', + 'hb_adid': '5691dd18ba6ab6', + 'hb_pb': '0.61', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '' + }, + 'status': 'rendered', + 'params': [ + { + 'placementId': 24428 + } + ] + }; + spec.onBidWon(bid); + expect(bid.nurl).to.deep.equal('nurl/?ap=0.61'); + }); + }); +}); diff --git a/test/spec/modules/mediafilterRtdProvider_spec.js b/test/spec/modules/mediafilterRtdProvider_spec.js new file mode 100644 index 00000000000..3395c7be691 --- /dev/null +++ b/test/spec/modules/mediafilterRtdProvider_spec.js @@ -0,0 +1,147 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; + +import { + MediaFilter, + MEDIAFILTER_EVENT_TYPE, + MEDIAFILTER_BASE_URL +} from '../../../modules/mediafilterRtdProvider.js'; + +describe('The Media Filter RTD module', function () { + describe('register()', function() { + let submoduleSpy, generateInitHandlerSpy; + + beforeEach(function () { + submoduleSpy = sinon.spy(hook, 'submodule'); + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + }); + + afterEach(function () { + submoduleSpy.restore(); + generateInitHandlerSpy.restore(); + }); + + it('should register and call the submodule function(s)', function () { + MediaFilter.register(); + + expect(submoduleSpy.calledOnceWithExactly('realTimeData', sinon.match.object)).to.be.true; + expect(submoduleSpy.called).to.be.true; + expect(generateInitHandlerSpy.called).to.be.true; + }); + }); + + describe('setup()', function() { + let setupEventListenerSpy, setupScriptSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + setupScriptSpy = sinon.spy(MediaFilter, 'setupScript'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + setupScriptSpy.restore(); + }); + + it('should call setupEventListener and setupScript function(s)', function() { + MediaFilter.setup({ configurationHash: 'abc123' }); + + expect(setupEventListenerSpy.called).to.be.true; + expect(setupScriptSpy.called).to.be.true; + }); + }); + + describe('setupEventListener()', function() { + let setupEventListenerSpy, addEventListenerSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + addEventListenerSpy.restore(); + }); + + it('should call addEventListener function(s)', function() { + MediaFilter.setupEventListener(); + expect(addEventListenerSpy.called).to.be.true; + expect(addEventListenerSpy.calledWith('message', sinon.match.func)).to.be.true; + }); + }); + + describe('generateInitHandler()', function() { + let generateInitHandlerSpy, setupMock, logErrorSpy; + + beforeEach(function() { + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + setupMock = sinon.stub(MediaFilter, 'setup').throws(new Error('Mocked error!')); + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + generateInitHandlerSpy.restore(); + setupMock.restore(); + logErrorSpy.restore(); + }); + + it('should handle errors in the catch block when setup throws an error', function() { + const initHandler = MediaFilter.generateInitHandler(); + initHandler({}); + + expect(logErrorSpy.calledWith('Error in initialization: Mocked error!')).to.be.true; + }); + }); + + describe('generateEventHandler()', function() { + let generateEventHandlerSpy, eventsEmitSpy; + + beforeEach(function() { + generateEventHandlerSpy = sinon.spy(MediaFilter, 'generateEventHandler'); + eventsEmitSpy = sinon.spy(events, 'emit'); + }); + + afterEach(function() { + generateEventHandlerSpy.restore(); + eventsEmitSpy.restore(); + }); + + it('should emit a billable event when the event type matches', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash) + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': sinon.match.string, + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + })).to.be.true; + }); + + it('should not emit a billable event when the event type does not match', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: 'differentEventType' + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.called).to.be.false; + }); + }); +}); diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index e77af544429..601bcff527a 100644 --- a/test/spec/modules/mediagoBidAdapter_spec.js +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -11,12 +11,49 @@ describe('mediago:BidAdapterTests', function () { bidder: 'mediago', params: { token: '85a6b01e41ac36d49744fad726e3655d', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://trace.mediago.io', bidfloor: 0.01, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA' + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name' + }, + pbadslot: '/12345/my-gpt-tag-0' + } + } + } }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left' + } + }, + ortb2: { + site: { + cat: ['IAB2'], + keywords: 'power tools, drills, tools=industrial', + content: { + keywords: 'video, source=streaming' + }, + }, + user: { + ext: { + data: {} + } + } }, adUnitCode: 'regular_iframe', transactionId: '7b26fdae-96e6-4c35-a18b-218dda11397d', @@ -27,9 +64,83 @@ describe('mediago:BidAdapterTests', function () { src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, - bidderWinsCount: 0, - }, + bidderWinsCount: 0 + } ], + gdprConsent: { + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + }, + userId: { + tdid: 'sample-userid', + uid2: { id: 'sample-uid2-value' }, + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid', + pubProvidedId: [ + { + source: 'puburl.com', + uids: [ + { + id: 'pubid2', + atype: 1, + ext: { + stype: 'ppuid' + } + } + ] + }, + { + source: 'puburl2.com', + uids: [ + { + id: 'pubid2' + }, + { + id: 'pubid2-123' + } + ] + } + ] + }, + userIdAsEids: [ + { + source: 'adserver.org', + uids: [{ id: 'sample-userid' }] + }, + { + source: 'criteo.com', + uids: [{ id: 'sample-criteo-userid' }] + }, + { + source: 'netid.de', + uids: [{ id: 'sample-netId-userid' }] + }, + { + source: 'liveramp.com', + uids: [{ id: 'sample-idl-userid' }] + }, + { + source: 'uidapi.com', + uids: [{ id: 'sample-uid2-value' }] + }, + { + source: 'puburl.com', + uids: [{ id: 'pubid1' }] + }, + { + source: 'puburl2.com', + uids: [{ id: 'pubid2' }, { id: 'pubid2-123' }] + } + ] }; let request = []; @@ -38,8 +149,8 @@ describe('mediago:BidAdapterTests', function () { spec.isBidRequestValid({ bidder: 'mediago', params: { - token: ['85a6b01e41ac36d49744fad726e3655d'], - }, + token: ['85a6b01e41ac36d49744fad726e3655d'] + } }) ).to.equal(true); }); @@ -51,10 +162,12 @@ describe('mediago:BidAdapterTests', function () { }); it('mediago:validate_response_params', function () { - let adm = ""; + let adm = + ''; let temp = '%3Cscr'; temp += 'ipt%3E'; - temp += '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; + temp += + '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; temp += '%3B%3C%2Fscri'; temp += 'pt%3E'; adm += decodeURIComponent(temp); @@ -72,13 +185,13 @@ describe('mediago:BidAdapterTests', function () { cid: '1339145', crid: 'ff32b6f9b3bbc45c00b78b6674a2952e', w: 300, - h: 250, - }, - ], - }, + h: 250 + } + ] + } ], - cur: 'USD', - }, + cur: 'USD' + } }; let bids = spec.interpretResponse(serverResponse); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 125d4bef02b..d7984c05967 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -101,6 +101,7 @@ describe('MediaSquare bid adapter tests', function () { 'adomain': ['test.com'], 'context': 'instream', 'increment': 1.0, + 'ova': 'cleared', }], }}; @@ -171,6 +172,7 @@ describe('MediaSquare bid adapter tests', function () { expect(bid.mediasquare.increment).to.exist; expect(bid.mediasquare.increment).to.equal(1.0); expect(bid.mediasquare.code).to.equal([DEFAULT_PARAMS[0].params.owner, DEFAULT_PARAMS[0].params.code].join('/')); + expect(bid.mediasquare.ova).to.exist.and.to.equal('cleared'); expect(bid.meta).to.exist; expect(bid.meta.advertiserDomains).to.exist; expect(bid.meta.advertiserDomains).to.have.lengthOf(1); @@ -213,6 +215,7 @@ describe('MediaSquare bid adapter tests', function () { let message = JSON.parse(server.requests[0].requestBody); expect(message).to.have.property('increment').exist; expect(message).to.have.property('increment').and.to.equal('1'); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); it('Verifies user sync without cookie in bid response', function () { var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); diff --git a/test/spec/modules/mgidXBidAdapter_spec.js b/test/spec/modules/mgidXBidAdapter_spec.js index 14619e9c0e1..978ca3de036 100644 --- a/test/spec/modules/mgidXBidAdapter_spec.js +++ b/test/spec/modules/mgidXBidAdapter_spec.js @@ -76,7 +76,10 @@ describe('MGIDXBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { referer: 'https://test.com' } @@ -131,7 +134,7 @@ describe('MGIDXBidAdapter', function () { expect(data.host).to.be.a('string'); expect(data.page).to.be.a('string'); expect(data.coppa).to.be.a('number'); - expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.be.a('object'); expect(data.ccpa).to.be.a('string'); expect(data.tmax).to.be.a('number'); expect(data.placements).to.have.lengthOf(3); @@ -172,8 +175,10 @@ describe('MGIDXBidAdapter', function () { serverRequest = spec.buildRequests(bids, bidderRequest); let data = serverRequest.data; expect(data.gdpr).to.exist; - expect(data.gdpr).to.be.a('string'); - expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.gdpr).to.be.a('object'); + expect(data.gdpr).to.have.property('consentString'); + expect(data.gdpr).to.not.have.property('vendorData'); + expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); expect(data.ccpa).to.not.exist; delete bidderRequest.gdprConsent; }); diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index 48f694bc79d..7e22114aeed 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -178,6 +178,16 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[1].mediaType).to.equal(BANNER) }); + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + it('should respect syncEnabled option', function() { config.setConfig({ userSync: { diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index f61987298e8..a51cb8bbac9 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -1,23 +1,64 @@ import { expect } from 'chai'; -import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec, storage } from 'modules/missenaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +const REFERRER = 'https://referer'; +const REFERRER2 = 'https://referer2'; describe('Missena Adapter', function () { - const adapter = newBidder(spec); + $$PREBID_GLOBAL$$.bidderSettings = { + missena: { + storageAllowed: true, + }, + }; const bidId = 'abc'; - const bid = { bidder: 'missena', bidId: bidId, sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, + params: { + apiKey: 'PA-34745704', + placement: 'sticky', + formats: ['sticky-banner'], + }, + getFloor: (inputParams) => { + if (inputParams.mediaType === BANNER) { + return { + currency: 'EUR', + floor: 3.5, + }; + } else { + return {}; + } + }, + }; + const bidWithoutFloor = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, params: { apiKey: 'PA-34745704', placement: 'sticky', formats: ['sticky-banner'], }, }; + const consentString = 'AAAAAAAAA=='; + + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + topmostLocation: REFERRER, + canonicalUrl: 'https://canonical', + }, + }; + const bids = [bid, bidWithoutFloor]; describe('codes', function () { it('should return a bidder code of missena', function () { expect(spec.code).to.equal('missena'); @@ -31,34 +72,27 @@ describe('Missena Adapter', function () { it('should return false if the apiKey is missing', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: {} })) + spec.isBidRequestValid(Object.assign(bid, { params: {} })), ).to.equal(false); }); it('should return false if the apiKey is an empty string', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })), ).to.equal(false); }); }); describe('buildRequests', function () { - const consentString = 'AAAAAAAAA=='; + let getDataFromLocalStorageStub = sinon.stub( + storage, + 'getDataFromLocalStorage', + ); - const bidderRequest = { - gdprConsent: { - consentString: consentString, - gdprApplies: true, - }, - refererInfo: { - topmostLocation: 'https://referer', - canonicalUrl: 'https://canonical', - }, - }; - - const requests = spec.buildRequests([bid, bid], bidderRequest); + const requests = spec.buildRequests(bids, bidderRequest); const request = requests[0]; const payload = JSON.parse(request.data); + const payloadNoFloor = JSON.parse(requests[1].data); it('should return as many server requests as bidder requests', function () { expect(requests.length).to.equal(2); @@ -81,7 +115,7 @@ describe('Missena Adapter', function () { }); it('should send referer information to the request', function () { - expect(payload.referer).to.equal('https://referer'); + expect(payload.referer).to.equal(REFERRER); expect(payload.referer_canonical).to.equal('https://canonical'); }); @@ -89,6 +123,66 @@ describe('Missena Adapter', function () { expect(payload.consent_string).to.equal(consentString); expect(payload.consent_required).to.equal(true); }); + it('should send floor data', function () { + expect(payload.floor).to.equal(3.5); + expect(payload.floor_currency).to.equal('EUR'); + }); + it('should not send floor data if not available', function () { + expect(payloadNoFloor.floor).to.equal(undefined); + expect(payloadNoFloor.floor_currency).to.equal(undefined); + }); + + getDataFromLocalStorageStub.restore(); + getDataFromLocalStorageStub = sinon.stub( + storage, + 'getDataFromLocalStorage', + ); + const localStorageData = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + }), + }; + getDataFromLocalStorageStub.callsFake((key) => localStorageData[key]); + const cappedRequests = spec.buildRequests(bids, bidderRequest); + + it('should not participate if capped', function () { + expect(cappedRequests.length).to.equal(0); + }); + + const localStorageDataSamePage = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + referer: REFERRER, + }), + }; + + getDataFromLocalStorageStub.callsFake( + (key) => localStorageDataSamePage[key], + ); + const cappedRequestsSamePage = spec.buildRequests(bids, bidderRequest); + + it('should not participate if capped on same page', function () { + expect(cappedRequestsSamePage.length).to.equal(0); + }); + + const localStorageDataOtherPage = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + referer: REFERRER2, + }), + }; + + getDataFromLocalStorageStub.callsFake( + (key) => localStorageDataOtherPage[key], + ); + const cappedRequestsOtherPage = spec.buildRequests(bids, bidderRequest); + + it('should participate if capped on a different page', function () { + expect(cappedRequestsOtherPage.length).to.equal(2); + }); }); describe('interpretResponse', function () { @@ -121,14 +215,14 @@ describe('Missena Adapter', function () { expect(result.length).to.equal(1); expect(Object.keys(result[0])).to.have.members( - Object.keys(serverResponse) + Object.keys(serverResponse), ); }); it('should return an empty response when the server answers with a timeout', function () { const result = spec.interpretResponse( { body: serverTimeoutResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); @@ -136,7 +230,7 @@ describe('Missena Adapter', function () { it('should return an empty response when the server answers with an empty ad', function () { const result = spec.interpretResponse( { body: serverEmptyAdResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js index 766f8d1a848..a4e58afbd1b 100644 --- a/test/spec/modules/mobfoxpbBidAdapter_spec.js +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -20,7 +20,8 @@ describe('MobfoxHBBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -143,6 +144,36 @@ describe('MobfoxHBBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index eaf8fa33a66..c11113473ce 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -1,16 +1,15 @@ import {expect} from 'chai'; import { - validateMultibid, - adjustBidderRequestsHook, addBidResponseHook, + adjustBidderRequestsHook, resetMultibidUnits, + resetMultiConfig, sortByMultibid, targetBidPoolHook, - resetMultiConfig + validateMultibid } from 'modules/multibid/index.js'; -import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; +import {getHighestCpm} from '../../../src/utils/reducers.js'; describe('multibid adapter', function () { let bidArray = [{ @@ -545,7 +544,7 @@ describe('multibid adapter', function () { it('it does not run filter on bidsReceived if no multibid configuration found', function () { let bids = [{...bidArray[0]}, {...bidArray[1]}]; - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -562,7 +561,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); bids.pop(); expect(result).to.not.equal(null); @@ -584,7 +583,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -609,7 +608,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -642,7 +641,7 @@ describe('multibid adapter', function () { config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm, 3); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -670,7 +669,7 @@ describe('multibid adapter', function () { expect(bidPool.length).to.equal(6); - targetBidPoolHook(callbackFn, bidPool, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bidPool, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); diff --git a/test/spec/modules/mygaruIdSystem_spec.js b/test/spec/modules/mygaruIdSystem_spec.js new file mode 100644 index 00000000000..2bfb5fdd4af --- /dev/null +++ b/test/spec/modules/mygaruIdSystem_spec.js @@ -0,0 +1,62 @@ +import { mygaruIdSubmodule } from 'modules/mygaruIdSystem.js'; +import { server } from '../../mocks/xhr'; + +describe('MygaruID module', function () { + it('should respond with async callback and get valid id', async () => { + const callBackSpy = sinon.spy(); + const expectedUrl = `https://ident.mygaru.com/v2/id?gdprApplies=0`; + const result = mygaruIdSubmodule.getId({}); + + expect(result.callback).to.be.an('function'); + const promise = result.callback(callBackSpy); + + const request = server.requests[0]; + expect(request.url).to.be.eq(expectedUrl); + + request.respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ iuid: '123' }) + ); + await promise; + + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledWith({mygaruId: '123'})).to.be.true; + }); + it('should not fail on error', async () => { + const callBackSpy = sinon.spy(); + const expectedUrl = `https://ident.mygaru.com/v2/id?gdprApplies=0`; + const result = mygaruIdSubmodule.getId({}); + + expect(result.callback).to.be.an('function'); + const promise = result.callback(callBackSpy); + + const request = server.requests[0]; + expect(request.url).to.be.eq(expectedUrl); + + request.respond( + 500, + {}, + '' + ); + await promise; + + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledWith({mygaruId: undefined})).to.be.true; + }); + + it('should not modify while decoding', () => { + const id = '222'; + const newId = mygaruIdSubmodule.decode(id) + + expect(id).to.eq(newId); + }) + it('should buildUrl with consent data', () => { + const result = mygaruIdSubmodule.getId({}, { + gdprApplies: true, + consentString: 'consentString' + }); + + expect(result.url).to.eq('https://ident.mygaru.com/v2/id?gdprApplies=1&gdprConsentString=consentString'); + }) +}); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 564788c8b56..f57db82aedc 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -1,32 +1,510 @@ import { expect } from 'chai'; -import { spec } from 'modules/nextMillenniumBidAdapter.js'; +import { + getImp, + replaceUsersyncMacros, + setConsentStrings, + setOrtb2Parameters, + setEids, + spec, +} from 'modules/nextMillenniumBidAdapter.js'; + +describe('nextMillenniumBidAdapterTests', () => { + describe('function getImp', () => { + const dataTests = [ + { + title: 'imp - banner', + data: { + id: '123', + bid: { + mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, + adUnitCode: 'test-banner-1', + }, + }, -describe('nextMillenniumBidAdapterTests', function() { - const bidRequestData = [ - { - adUnitCode: 'test-div', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { placement_id: '-1' }, - sizes: [[300, 250]], - uspConsent: '1---', - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true + expected: { + id: 'test-banner-1', + ext: {prebid: {storedrequest: {id: '123'}}}, + banner: {format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + }, }, - ortb2: { - device: { - w: 1500, - h: 1000 + + { + title: 'imp - video', + data: { + id: '234', + bid: { + mediaTypes: {video: {playerSize: [400, 300]}}, + adUnitCode: 'test-video-1', + }, }, - site: { - domain: 'example.com', - page: 'http://example.com' - } + + expected: { + id: 'test-video-1', + ext: {prebid: {storedrequest: {id: '234'}}}, + video: {w: 400, h: 300}, + }, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {bid, id} = data; + const imp = getImp(bid, id); + expect(imp).to.deep.equal(expected); + }); + } + }); + + describe('function setConsentStrings', () => { + const dataTests = [ + { + title: 'full: uspConsent, gdprConsent and gppConsent', + data: { + postBody: {}, + bidderRequest: { + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + }, + }, + + expected: { + user: {ext: {consent: 'kjfdniwjnifwenrif3'}}, + regs: { + gpp: 'DBACNYA~CPXxRfAPXxR', + gpp_sid: [7], + ext: {gdpr: 1, us_privacy: '1---'}, + }, + }, + }, + + { + title: 'gdprConsent(false) and ortb2(gpp)', + data: { + postBody: {}, + bidderRequest: { + gdprConsent: {consentString: 'ewtewbefbawyadexv', gdprApplies: false}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + }, + }, + + expected: { + user: {ext: {consent: 'ewtewbefbawyadexv'}}, + regs: { + gpp: 'DSFHFHWEUYVDC', + gpp_sid: [8, 9, 10], + ext: {gdpr: 0}, + }, + }, + }, + + { + title: 'gdprConsent(false)', + data: { + postBody: {}, + bidderRequest: {gdprConsent: {gdprApplies: false}}, + }, + + expected: { + regs: {ext: {gdpr: 0}}, + }, + }, + + { + title: 'empty', + data: { + postBody: {}, + bidderRequest: {}, + }, + + expected: {}, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {postBody, bidderRequest} = data; + setConsentStrings(postBody, bidderRequest); + expect(postBody).to.deep.equal(expected); + }); + } + }); + + describe('function replaceUsersyncMacros', () => { + const dataTests = [ + { + title: 'url with all macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + type: 'image', + }, + + expected: 'https://some.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=image', + }, + + { + title: 'url with some macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&type={{.TYPE_PIXEL}}', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: false}, + type: 'iframe', + }, + + expected: 'https://some.url?gdpr=0&gdpr_consent=kjfdniwjnifwenrif3&type=iframe', + }, + + { + title: 'url without macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?param1=value1¶m2=value2', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: false}, + type: 'iframe', + }, + + expected: 'https://some.url?param1=value1¶m2=value2', + }, + + { + title: 'url with all macroses - consents are empty', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}', + }, + + expected: 'https://some.url?gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid=&type=', + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {url, gdprConsent, uspConsent, gppConsent, type} = data; + const newUrl = replaceUsersyncMacros(url, gdprConsent, uspConsent, gppConsent, type); + expect(newUrl).to.equal(expected); + }); + } + }); + + describe('function spec.getUserSyncs', () => { + const dataTests = [ + { + title: 'pixels from responses ({iframeEnabled: true, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: true}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + + {body: {ext: {sync: { + iframe: [ + 'https://some.6.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.7.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + + {body: {ext: {sync: { + image: [ + 'https://some.8.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://some.1.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.2.url?us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.3.url?param=1234'}, + {type: 'iframe', url: 'https://some.4.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.5.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + {type: 'iframe', url: 'https://some.6.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.7.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + {type: 'image', url: 'https://some.8.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + ], + }, + + { + title: 'pixels from responses ({iframeEnabled: true, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: false}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'iframe', url: 'https://some.4.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.5.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + ], + }, + + { + title: 'pixels from responses ({iframeEnabled: false, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: false, pixelEnabled: true}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://some.1.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.2.url?us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.3.url?param=1234'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: true, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: true}, + responses: [], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=image'}, + {type: 'iframe', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=iframe'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: true, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: false}, + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'iframe', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=iframe'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: false, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: false, pixelEnabled: false}, + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [], + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {syncOptions, responses, gdprConsent, uspConsent, gppConsent} = data; + const pixels = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent); + expect(pixels).to.deep.equal(expected); + }); + } + }); + + describe('function setOrtb2Parameters', () => { + const dataTests = [ + { + title: 'site.pagecat, site.content.cat and site.content.language', + data: { + postBody: {}, + ortb2: {site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + }}, + }, + + expected: {site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + }}, + }, + + { + title: 'only site.content.language', + data: { + postBody: {site: {domain: 'some.domain'}}, + ortb2: {site: { + content: {language: 'EN'}, + }}, + }, + + expected: {site: { + domain: 'some.domain', + content: {language: 'EN'}, + }}, + }, + + { + title: 'object ortb2 is empty', + data: { + postBody: {imp: []}, + }, + + expected: {imp: []}, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {postBody, ortb2} = data; + setOrtb2Parameters(postBody, ortb2); + expect(postBody).to.deep.equal(expected); + }); + }; + }); + + describe('function setEids', () => { + const dataTests = [ + { + title: 'setEids - userIdAsEids is empty', + data: { + postBody: {}, + bid: { + userIdAsEids: undefined, + }, + }, + + expected: {}, + }, + + { + title: 'setEids - userIdAsEids - array is empty', + data: { + postBody: {}, + bid: { + userIdAsEids: [], + }, + }, + + expected: {}, + }, + + { + title: 'setEids - userIdAsEids is', + data: { + postBody: {}, + bid: { + userIdAsEids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + }, + + expected: { + user: { + eids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + }, + }, + ]; + + for (let { title, data, expected } of dataTests) { + it(title, () => { + const { postBody, bid } = data; + setEids(postBody, bid); + expect(postBody).to.deep.equal(expected); + }); + } + }); + + const bidRequestData = [{ + adUnitCode: 'test-div', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { placement_id: '-1' }, + sizes: [[300, 250]], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + }, + + ortb2: { + device: { + w: 1500, + h: 1000 + }, + + site: { + domain: 'example.com', + page: 'http://example.com' } } - ]; + }]; const serverResponse = { body: { @@ -49,7 +527,7 @@ describe('nextMillenniumBidAdapterTests', function() { cur: 'USD', ext: { sync: { - image: ['urlA?gdpr={{.GDPR}}'], + image: ['urlA?gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}'], iframe: ['urlB'], } } @@ -117,61 +595,6 @@ describe('nextMillenniumBidAdapterTests', function() { }, ]; - it('Request params check with GDPR and USP Consent', function () { - const request = spec.buildRequests(bidRequestData, bidRequestData[0]); - expect(JSON.parse(request[0].data).user.ext.consent).to.equal('kjfdniwjnifwenrif3'); - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); - }); - - it('Test getUserSyncs function', function () { - const syncOptions = { - 'iframeEnabled': false, - 'pixelEnabled': true - } - let userSync = spec.getUserSyncs(syncOptions, [serverResponse], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.equal('image'); - expect(userSync[0].url).to.equal('urlA?gdpr=1'); - - syncOptions.iframeEnabled = true; - syncOptions.pixelEnabled = false; - userSync = spec.getUserSyncs(syncOptions, [serverResponse], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.equal('iframe'); - expect(userSync[0].url).to.equal('urlB'); - }); - - it('Test getUserSyncs with no response', function () { - const syncOptions = { - 'iframeEnabled': true, - 'pixelEnabled': false - } - let userSync = spec.getUserSyncs(syncOptions, [], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array') - expect(userSync[0].type).to.equal('iframe') - expect(userSync[0].url).to.equal('https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&type=iframe') - }) - - it('Test getUserSyncs function if GDPR is undefined', function () { - const syncOptions = { - 'iframeEnabled': false, - 'pixelEnabled': true - } - - let userSync = spec.getUserSyncs(syncOptions, [serverResponse], undefined, bidRequestData[0].uspConsent); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.equal('image'); - expect(userSync[0].url).to.equal('urlA?gdpr=0'); - }); - - it('Request params check without GDPR Consent', function () { - delete bidRequestData[0].gdprConsent - const request = spec.buildRequests(bidRequestData, bidRequestData[0]); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); - }); - it('validate_generated_params', function() { const request = spec.buildRequests(bidRequestData, {bidderRequestId: 'mock-uuid'}); expect(request[0].bidId).to.equal('bid1234'); @@ -190,7 +613,7 @@ describe('nextMillenniumBidAdapterTests', function() { it('Check if refresh_count param is incremented', function() { const request = spec.buildRequests(bidRequestData); - expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); + expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(1); }); it('Check if domain was added', function() { diff --git a/test/spec/modules/nobidAnalyticsAdapter_spec.js b/test/spec/modules/nobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..06a39ffd020 --- /dev/null +++ b/test/spec/modules/nobidAnalyticsAdapter_spec.js @@ -0,0 +1,498 @@ +import nobidAnalytics from 'modules/nobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +let events = require('src/events'); +let adapterManager = require('src/adapterManager').default; +let constants = require('src/constants.json'); + +const TOP_LOCATION = 'https://www.somesite.com'; +const SITE_ID = 1234; + +describe('NoBid Prebid Analytic', function () { + var clock; + describe('enableAnalytics', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('auctionInit test', function (done) { + const initOptions = { + options: { + /* siteId: SITE_ID */ + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions).to.equal(undefined); + + initOptions.options.siteId = SITE_ID; + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions.siteId).to.equal(SITE_ID); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + expect(nobidAnalytics.initOptions).to.have.property('siteId', SITE_ID); + expect(nobidAnalytics).to.have.property('topLocation', TOP_LOCATION); + + const data = { ts: Date.now() }; + clock.tick(5000); + const expired = nobidAnalytics.isExpired(data); + expect(expired).to.equal(false); + + done(); + }); + + it('BID_REQUESTED/BID_RESPONSE/BID_TIMEOUT/AD_RENDER_SUCCEEDED test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + events.emit(constants.EVENTS.BID_WON, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_REQUESTED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_RESPONSE, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + done(); + }); + + it('bidWon test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + bidderCode: 'nobid', + width: 728, + height: 9, + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + transactionId: 'd58cbeae-92c8-4262-ba8d-0e649cbf5470', + auctionId: 'd758cce5-d178-408c-b777-8cac605ef7ca', + mediaType: 'banner', + source: 'client', + cpm: 6.4, + creativeId: 'TEST', + dealId: '', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: 'AD HERE', + meta: { + advertiserDomains: ['advertiser_domain.com'] + }, + metrics: { + 'requestBids.usp': 0 + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692156287517, + requestTimestamp: 1692156286972, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 545, + pbCg: '', + size: '728x90', + adserverTargeting: { + hb_bidder: 'nobid', + hb_adid: '106d14b7d06b607', + hb_pb: '6.40', + hb_size: '728x90', + hb_source: 'client', + hb_format: 'banner', + hb_adomain: 'advertiser_domain.com', + 'hb_crid': 'TEST' + }, + status: 'rendered', + params: [ + { + siteId: SITE_ID + } + ] + }; + + const requestOutgoing = { + bidderCode: 'nobid', + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + mediaType: 'banner', + cpm: 6.4, + adUnitCode: 'leaderboard', + timeToRespond: 545, + size: '728x90', + topLocation: TOP_LOCATION + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.BID_WON, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const bidWonRequest = JSON.parse(server.requests[0].requestBody); + expect(bidWonRequest).to.have.property('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.not.have.property('pbCg'); + + done(); + }); + + it('auctionEnd test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + timestamp: 1692224437573, + auctionEnd: 1692224437986, + auctionStatus: 'completed', + adUnits: [ + { + code: 'leaderboard', + sizes: [[728, 90]], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[300, 250]] }, + { minViewPort: [750, 0], sizes: [[728, 90]] } + ], + adunit: '/111111/adunit', + bids: [{ bidder: 'nobid', params: { siteId: SITE_ID } }] + } + ], + adUnitCodes: ['leaderboard'], + bidderRequests: [ + { + bidderCode: 'nobid', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequestId: '5beedb9f99ad98', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + sizes: [[728, 90]], + bidId: '6ef0277f36c8df', + bidderRequestId: '5beedb9f99ad98', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2: { + site: { + domain: 'site.me', + publisher: { + domain: 'site.me' + }, + page: TOP_LOCATION + }, + device: { + w: 2605, + h: 895, + dnt: 0, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + language: 'en', + } + } + } + ], + auctionStart: 1692224437573, + timeout: 3000, + refererInfo: { + topmostLocation: TOP_LOCATION, + location: TOP_LOCATION, + page: TOP_LOCATION, + domain: 'site.me', + ref: null, + } + } + ], + noBids: [ + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + statusMessage: 'Bid available', + adId: '95781b6ae5ef2f', + requestId: '6ef0277f36c8df', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + mediaType: 'banner', + source: 'client', + cpm: 6.44, + creativeId: 'TEST', + dealId: '', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: '', + meta: { + advertiserDomains: [ + 'advertiser_domain.com' + ] + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692224437982, + requestTimestamp: 1692224437576, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 0, + pbLg: 5.00, + pbCg: '', + size: '728x90', + adserverTargeting: { hb_bidder: 'nobid', hb_pb: '6.40' }, + status: 'targetingSet' + } + ], + bidsRejected: [], + winningBids: [], + timeout: 3000 + }; + + const requestOutgoing = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequests: [ + { + bidderCode: 'nobid', + bidderRequestId: '7c1940bb285731', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + sizes: [[728, 90]], + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1 + } + ], + refererInfo: { + topmostLocation: TOP_LOCATION + } + } + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 6.44, + adUnitCode: 'leaderboard' + } + ] + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: `${TOP_LOCATION}_something`}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.AUCTION_END, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const auctionEndRequest = JSON.parse(server.requests[0].requestBody); + expect(auctionEndRequest).to.have.property('auctionId', requestOutgoing.auctionId); + expect(auctionEndRequest.bidderRequests).to.have.length(1); + expect(auctionEndRequest.bidderRequests[0]).to.have.property('bidderCode', requestOutgoing.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); + + done(); + }); + + it('Analytics disabled test', function (done) { + let disabled; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: false})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678901'}); + clock.tick(1000); + expect(server.requests).to.have.length(2); + + nobidAnalytics.processServerResponse('disabled: true'); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(1000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(5000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.retentionSeconds = 5; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + clock.tick(1000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + clock.tick(6000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + + done(); + }); + }); + + describe('NoBid Carbonizer', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('Carbonizer test', function (done) { + let active = nobidCarbonizer.isActive(); + expect(active).to.equal(false); + + active = nobidCarbonizer.isActive(JSON.stringify({carbonizer_active: false})); + expect(active).to.equal(false); + + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(true); + + const previousRetention = nobidAnalytics.retentionSeconds; + nobidAnalytics.retentionSeconds = 3; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + let stored = nobidCarbonizer.getStoredLocalData(); + expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain(`{"carbonizer_active":true,"ts":`); + clock.tick(5000); + active = nobidCarbonizer.isActive(adunits, true); + expect(active).to.equal(false); + + nobidAnalytics.retentionSeconds = previousRetention; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(adunits, true); + expect(active).to.equal(true); + + let adunits = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + nobidCarbonizer.carbonizeAdunits(adunits, true); + stored = nobidCarbonizer.getStoredLocalData(); + expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain('{"carbonizer_active":true,"ts":'); + expect(stored[nobidAnalytics.ANALYTICS_OPT_NAME]).to.contain('{"bidder1":1,"bidder2":1}'); + clock.tick(5000); + expect(adunits[0].bids.length).to.equal(0); + + done(); + }); + }); +}); diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index ed358af19b6..aad753571a8 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -42,7 +42,21 @@ describe('OguryBidAdapter', function () { return floorResult; }, - transactionId: 'transactionId' + transactionId: 'transactionId', + userId: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + userIdAsEids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ] }, { adUnitCode: 'adUnitCode2', @@ -407,12 +421,26 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, + uids: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + eids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ], }, }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.5.0' + adapterversion: '1.6.0' }, device: { w: stubbedWidth, @@ -637,7 +665,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -663,7 +693,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -689,7 +721,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -701,6 +735,48 @@ describe('OguryBidAdapter', function () { expect(request.data.regs.ext.gdpr).to.be.a('number'); }); + it('should should not add uids infos if userId is undefined', () => { + const expectedRequestWithUndefinedUserId = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + eids: expectedRequestObject.user.ext.eids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userId: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserId); + }); + + it('should should not add uids infos if userIdAsEids is undefined', () => { + const expectedRequestWithUndefinedUserIdAsEids = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + uids: expectedRequestObject.user.ext.uids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userIdAsEids: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserIdAsEids); + }); + it('should handle bidFloor undefined', () => { const expectedRequestWithUndefinedFloor = { ...expectedRequestObject @@ -814,7 +890,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.5.0', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -831,7 +907,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.5.0', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }] diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index df6456db82e..7960574c1a4 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -15,9 +15,14 @@ describe('onetag', function () { 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', - ortb2Imp: { - ext: { - tid: 'qwerty123' + 'ortb2Imp': { + 'ext': { + 'tid': '0000' + } + }, + 'ortb2': { + 'source': { + 'tid': '1111' } }, 'schain': { @@ -184,7 +189,7 @@ describe('onetag', function () { }); it('Should contain all keys', function () { expect(data).to.be.an('object'); - expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version'); + expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version', 'fledgeEnabled'); expect(data.location).to.satisfy(function (value) { return value === null || typeof value === 'string'; }); @@ -208,6 +213,7 @@ describe('onetag', function () { expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { return value === null || typeof value === 'string' }); + expect(data.fledgeEnabled).to.be.a('boolean'); expect(data.bids).to.be.an('array'); expect(data.version).to.have.all.keys('prebid', 'adapter'); const bids = data['bids']; @@ -256,6 +262,15 @@ describe('onetag', function () { expect(dataObj.bids).to.be.an('array').that.is.empty; } catch (e) { } }); + it('Should pick each bid\'s auctionId and transactionId from ortb2 related fields', function () { + const serverRequest = spec.buildRequests([bannerBid]); + const payload = JSON.parse(serverRequest.data); + + expect(payload).to.exist; + expect(payload.bids).to.exist.and.to.have.length(1); + expect(payload.bids[0].auctionId).to.equal(bannerBid.ortb2.source.tid); + expect(payload.bids[0].transactionId).to.equal(bannerBid.ortb2Imp.ext.tid); + }); it('should send GDPR consent data', function () { let consentString = 'consentString'; let bidderRequest = { @@ -382,13 +397,61 @@ describe('onetag', function () { 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); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + }); + it('Should send FLEDGE eligibility flag when FLEDGE is not enabled', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'fledgeEnabled': false + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + }); + it('Should send FLEDGE eligibility flag set to false when fledgeEnabled is not defined', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(false); + }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); const response = getBannerVideoResponse(); + const fledgeResponse = getFledgeBannerResponse(); const requestData = JSON.parse(request.data); it('Returns an array of valid server responses if response object is valid', function () { const interpretedResponse = spec.interpretResponse(response, request); + const fledgeInterpretedResponse = spec.interpretResponse(fledgeResponse, request); expect(interpretedResponse).to.be.an('array').that.is.not.empty; + expect(fledgeInterpretedResponse).to.be.an('object'); + expect(fledgeInterpretedResponse.bids).to.satisfy(function (value) { + return value === null || Array.isArray(value); + }); + expect(fledgeInterpretedResponse.fledgeAuctionConfigs).to.be.an('array').that.is.not.empty; for (let i = 0; i < interpretedResponse.length; i++) { let dataItem = interpretedResponse[i]; expect(dataItem).to.include.all.keys('requestId', 'cpm', 'width', 'height', 'ttl', 'creativeId', 'netRevenue', 'currency', 'meta', 'dealId'); @@ -586,6 +649,24 @@ function getBannerVideoResponse() { }; } +function getFledgeBannerResponse() { + const bannerVideoResponse = getBannerVideoResponse(); + bannerVideoResponse.body.fledgeAuctionConfigs = [ + { + bidId: 'fledge', + config: { + seller: 'https://onetag-sys.com', + decisionLogicUrl: + 'https://onetag-sys.com/paapi/decision_logic.js', + interestGroupBuyers: [ + 'https://onetag-sys.com' + ], + } + } + ] + return bannerVideoResponse; +} + function getBannerVideoRequest() { return { data: JSON.stringify({ diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index f2cff7f470c..1af0fce103d 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -14,6 +14,7 @@ import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import {deepClone} from 'src/utils.js'; +import {version} from 'package.json'; import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {hook} from '../../../src/hook.js'; @@ -316,6 +317,7 @@ describe('OpenxRtbAdapter', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); expect(request[0].url).to.equal(REQUEST_URL); expect(request[0].method).to.equal('POST'); + expect(request[0].data.ext.pv).to.equal(version); }); it('should send delivery domain, if available', function () { diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index 37d4a2c7bc0..9a8981235d5 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -266,6 +266,95 @@ describe('Opera Ads Bid Adapter', function () { } }); + describe('test fulfilling inventory information', function () { + const bidRequest = { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: {sizes: [[300, 250]]} + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + } + + const getRequest = function () { + let reqs; + expect(function () { + reqs = spec.buildRequests([bidRequest], bidderRequest); + }).to.not.throw(); + return JSON.parse(reqs[0].data); + } + + it('test default case', function () { + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.domain).to.not.be.empty; + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with site information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-1', + domain: 'www.test.com' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-1'); + expect(requestData.site.domain).to.equal('www.test.com'); + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.app).to.be.an('object'); + expect(requestData.app.id).to.equal(bidRequest.params.publisherId); + expect(requestData.app.name).to.equal('test-app-1'); + expect(requestData.app.domain).to.not.be.empty; + }); + + it('test a case with both site and app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-2', + page: 'test-page' + }, + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-2'); + expect(requestData.site.page).to.equal('test-page'); + expect(requestData.site.domain).to.not.be.empty; + }); + }); + it('test getBidFloor', function() { const bidRequests = [ { diff --git a/test/spec/modules/optidigitalBidAdapter_spec.js b/test/spec/modules/optidigitalBidAdapter_spec.js index 62c37f85cc8..30e72452c39 100755 --- a/test/spec/modules/optidigitalBidAdapter_spec.js +++ b/test/spec/modules/optidigitalBidAdapter_spec.js @@ -479,6 +479,28 @@ describe('optidigitalAdapterTests', function () { expect(payload.imp[0].bidFloor).to.exist; }); + it('should add userEids to payload', function() { + const userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: '121213434342343', + atype: 1 + }] + }]; + validBidRequests[0].userIdAsEids = userIdAsEids; + bidderRequest.userIdAsEids = userIdAsEids; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user.eids).to.deep.equal(userIdAsEids); + }); + + it('should not add userIdAsEids to payload when userIdAsEids is not present', function() { + validBidRequests[0].userIdAsEids = undefined; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user).to.deep.equal(undefined); + }); + function returnBannerSizes(mediaTypes, expectedSizes) { const bidRequest = Object.assign(validBidRequests[0], mediaTypes); const request = spec.buildRequests([bidRequest], bidderRequest); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index aec8b79045e..8a9f000bbb9 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -21,13 +21,80 @@ describe('Optimera RTD sub module', () => { }); }); -describe('Optimera RTD score file url is properly set', () => { - it('Proerly set the score file url', () => { +describe('Optimera RTD score file URL is properly set for v0', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v0', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL without apiVersion set', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL with an api version other than v0 or v1', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v15', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); }); }); +describe('Optimera RTD score file URL is properly set for v1', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v1', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v1'); + expect(optimeraRTD.scoresURL).to.equal('https://v1.oapi26b.com/api/products/scores?c=9999&h=localhost:9876&p=/context.html&s=de'); + }); +}); + describe('Optimera RTD score file properly sets targeting values', () => { const scores = { 'div-0': ['A1', 'A2'], diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index acb779b436d..cf58d35e636 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; -import {spec} from 'modules/orbidderBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; +import { expect } from 'chai'; +import { spec } from 'modules/orbidderBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import * as _ from 'lodash'; import { BANNER, NATIVE } from '../../../src/mediaTypes.js'; @@ -9,39 +9,57 @@ describe('orbidderBidAdapter', () => { const defaultBidRequestBanner = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bb', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d8', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c6', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code', sizes: [[300, 250], [300, 600]], params: { 'accountId': 'string1', - 'placementId': 'string2' + 'placementId': 'string2', + 'bidfloor': 1.23 }, mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]], + sizes: [[300, 250], [300, 600]] } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*XXXXXXXXXXXXX', + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*XXXXXXXXXXXXX', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + ] + } + ] }; const defaultBidRequestNative = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bc', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d9', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c7', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code-native', sizes: [], params: { 'accountId': 'string3', - 'placementId': 'string4' + 'placementId': 'string4', + 'bidfloor': 2.34 }, mediaTypes: { native: { @@ -56,10 +74,34 @@ describe('orbidderBidAdapter', () => { required: true } } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*YYYYYYYYYYYYYYY', + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*YYYYYYYYYYYYYYY', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + ] + } + ] }; - const deepClone = function (val) { + const deepClone = function(val) { return JSON.parse(JSON.stringify(val)); }; @@ -91,15 +133,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(defaultBidRequestNative)).to.equal(true); }); - it('banner: accepts optional profile object', () => { + it('banner: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); - it('native: accepts optional profile object', () => { + it('native: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); @@ -115,15 +157,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('banner: doesn\'t accept malformed profile', () => { + it('banner: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('native: doesn\'t accept malformed profile', () => { + it('native: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); @@ -179,34 +221,20 @@ describe('orbidderBidAdapter', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(request.data).length).to.equal(Object.keys(defaultBidRequestBanner).length + 2); - expect(request.data.bidId).to.equal(defaultBidRequestBanner.bidId); - expect(request.data.auctionId).to.equal(defaultBidRequestBanner.auctionId); - expect(request.data.transactionId).to.equal(defaultBidRequestBanner.ortb2Imp.ext.tid); - expect(request.data.bidRequestCount).to.equal(defaultBidRequestBanner.bidRequestCount); - expect(request.data.adUnitCode).to.equal(defaultBidRequestBanner.adUnitCode); - expect(request.data.pageUrl).to.equal('https://localhost:9876/'); - expect(request.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(request.data.sizes).to.equal(defaultBidRequestBanner.sizes); - - expect(_.isEqual(request.data.params, defaultBidRequestBanner.params)).to.be.true; - expect(_.isEqual(request.data.mediaTypes, defaultBidRequestBanner.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestBanner); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(request.data).to.deep.equal(expectedBidRequest); }); it('native: sends correct bid parameters', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(nativeRequest.data).length).to.equal(Object.keys(defaultBidRequestNative).length + 2); - expect(nativeRequest.data.bidId).to.equal(defaultBidRequestNative.bidId); - expect(nativeRequest.data.auctionId).to.equal(defaultBidRequestNative.auctionId); - expect(nativeRequest.data.transactionId).to.equal(defaultBidRequestNative.ortb2Imp.ext.tid); - expect(nativeRequest.data.bidRequestCount).to.equal(defaultBidRequestNative.bidRequestCount); - expect(nativeRequest.data.adUnitCode).to.equal(defaultBidRequestNative.adUnitCode); - expect(nativeRequest.data.pageUrl).to.equal('https://localhost:9876/'); - expect(nativeRequest.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(nativeRequest.data.sizes).to.be.empty; - - expect(_.isEqual(nativeRequest.data.params, defaultBidRequestNative.params)).to.be.true; - expect(_.isEqual(nativeRequest.data.mediaTypes, defaultBidRequestNative.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestNative); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(nativeRequest.data).to.deep.equal(expectedBidRequest); }); it('banner: handles empty gdpr object', () => { @@ -347,7 +375,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; }); @@ -387,7 +415,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); Object.keys(expectedResponse[0]).forEach((key) => { @@ -454,7 +482,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; @@ -474,7 +502,7 @@ describe('orbidderBidAdapter', () => { 'netRevenue': true, } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -492,7 +520,7 @@ describe('orbidderBidAdapter', () => { 'creativeId': '29681110', } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -518,13 +546,13 @@ describe('orbidderBidAdapter', () => { } } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); it('handles nobid responses', () => { const serverResponse = []; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); }); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index d8690aeb6a5..e6abb5e9caa 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/outbrainBidAdapter.js'; +import { spec, storage } from 'modules/outbrainBidAdapter.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr'; @@ -213,15 +213,18 @@ describe('Outbrain Adapter', function () { }) describe('buildRequests', function () { + let getDataFromLocalStorageStub; + before(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') config.setConfig({ outbrain: { bidderUrl: 'https://bidder-url.com', } - } - ) + }) }) after(() => { + getDataFromLocalStorageStub.restore() config.resetConfig() }) @@ -424,7 +427,7 @@ describe('Outbrain Adapter', function () { expect(resData.badv).to.deep.equal(['bad-advertiser']) }); - it('first party data', function () { + it('should pass first party data', function () { const bidRequest = { ...commonBidRequest, ...nativeBidRequestParams, @@ -505,6 +508,28 @@ describe('Outbrain Adapter', function () { config.resetConfig() }); + it('should pass gpp information', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + }; + const bidderRequest = { + ...commonBidderRequest, + 'gppConsent': { + 'gppString': 'abc12345', + 'applicableSections': [8] + } + } + + const res = spec.buildRequests([bidRequest], bidderRequest); + const resData = JSON.parse(res.data); + + expect(resData.regs.ext.gpp).to.exist; + expect(resData.regs.ext.gpp_sid).to.exist; + expect(resData.regs.ext.gpp).to.equal('abc12345'); + expect(resData.regs.ext.gpp_sid).to.deep.equal([8]); + }); + it('should pass extended ids', function () { let bidRequest = { bidId: 'bidId', @@ -522,6 +547,22 @@ describe('Outbrain Adapter', function () { ]); }); + it('should pass OB user token', function () { + getDataFromLocalStorageStub.returns('12345'); + + let bidRequest = { + bidId: 'bidId', + params: {}, + ...commonBidRequest, + }; + + let res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data) + expect(resData.user.ext.obusertoken).to.equal('12345') + expect(getDataFromLocalStorageStub.called).to.be.true; + sinon.assert.calledWith(getDataFromLocalStorageStub, 'OB-USER-TOKEN'); + }); + it('should pass bidfloor', function () { const bidRequest = { ...commonBidRequest, @@ -842,6 +883,12 @@ describe('Outbrain Adapter', function () { type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); + + it('should pass gpp consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '', { gppString: 'abc12345', applicableSections: [1, 2] })).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gpp=abc12345&gpp_sid=1%2C2` + }]); + }); }) describe('onBidWon', function () { diff --git a/test/spec/modules/oxxionAnalyticsAdapter_spec.js b/test/spec/modules/oxxionAnalyticsAdapter_spec.js index 13dc395968a..9d06be24f68 100644 --- a/test/spec/modules/oxxionAnalyticsAdapter_spec.js +++ b/test/spec/modules/oxxionAnalyticsAdapter_spec.js @@ -86,20 +86,21 @@ describe('Oxxion Analytics', function () { } }, 'adUnitCode': 'tag_200124_banner', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'sizes': [ [ 300, 600 ] ], - 'bidId': '34a63e5d5378a3', + 'bidId': '2bd3e8ff8a113f', 'bidderRequestId': '11dc6ff6378de7', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 + 'bidderWinsCount': 0, + 'ova': 'cleared' } ], 'auctionStart': 1647424261187, @@ -149,12 +150,12 @@ describe('Oxxion Analytics', function () { 'bidsReceived': [ { 'bidderCode': 'appnexus', - 'width': 300, - 'height': 600, + 'width': 970, + 'height': 250, 'statusMessage': 'Bid available', - 'adId': '7a4ced80f33d33', - 'requestId': '34a63e5d5378a3', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'adId': '65d16ef039a97a', + 'requestId': '2bd3e8ff8a113f', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', 'mediaType': 'video', 'source': 'client', @@ -187,7 +188,7 @@ describe('Oxxion Analytics', function () { 'size': '300x600', 'adserverTargeting': { 'hb_bidder': 'appnexus', - 'hb_adid': '7a4ced80f33d33', + 'hb_adid': '65d16ef039a97a', 'hb_pb': '20.000000', 'hb_size': '300x600', 'hb_source': 'client', @@ -342,7 +343,7 @@ describe('Oxxion Analytics', function () { expect(message).to.have.property('adId') expect(message).to.have.property('cpmIncrement').and.to.equal(27.4276); expect(message).to.have.property('oxxionMode').and.to.have.property('abtest').and.to.equal(true); - // sinon.assert.callCount(oxxionAnalytics.track, 1); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); }); }); diff --git a/test/spec/modules/oxxionRtdProvider_spec.js b/test/spec/modules/oxxionRtdProvider_spec.js index 7bccf2319a4..2a8024f3565 100644 --- a/test/spec/modules/oxxionRtdProvider_spec.js +++ b/test/spec/modules/oxxionRtdProvider_spec.js @@ -113,77 +113,6 @@ let bids = [{ }, ]; -let originalBidderRequests = [{ - 'bidderCode': 'rubicon', - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'bidderRequestId': '16c2bceb2e891a', - 'bids': [ - { - 'bidder': 'rubicon', - 'params': { - 'accountId': 1234, - 'siteId': 2345, - 'zoneId': 3456 - }, - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'mediaTypes': {'banner': {'sizes': [[970, 250]]}}, - 'adUnitCode': 'adunit1', - 'transactionId': '8f20b49c-5e47-4bb5-a7d5-0b816cf527f3', - 'bidId': '2d9920072ab028', - 'bidderRequestId': '16c2bceb2e891a', - }, - { - 'bidder': 'rubicon', - 'params': { - 'accountId': 1234, - 'siteId': 2345, - 'zoneId': 4567 - }, - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, - 'adUnitCode': 'adunit2', - 'transactionId': '4161f09e-7870-4486-b2a6-b4158a327bc4', - 'bidId': '331c3d708f4864', - 'bidderRequestId': '16c2bceb2e891a', - 'src': 'client', - } - ], - 'auctionStart': 1683383333809, - 'timeout': 3000, - 'gdprConsent': { - 'consentString': 'consent_hash', - 'gdprApplies': true, - 'apiVersion': 2 - } -}, -{ - 'bidderCode': 'appnexusAst', - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'bidderRequestId': '4d83b8c60d45e7', - 'bids': [ - { - 'bidder': 'appnexusAst', - 'params': { - 'placementId': 10471298 - }, - 'auctionId': 'dd42b870-2072-4b71-8ab7-e7789b14c5ce', - 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, - 'adUnitCode': 'adunit2', - 'transactionId': '4161f09e-7870-4486-b2a6-b4158a327bc4', - 'bidId': '5b7cd5abc6aea3', - 'bidderRequestId': '4d83b8c60d45e7', - } - ], - 'auctionStart': 1683383333809, - 'timeout': 3000, - 'gdprConsent': { - 'consentString': 'consent_hash', - 'gdprApplies': true, - 'apiVersion': 2 - } -} -]; - let bidInterests = [ {'id': 0, 'rate': 50.0, 'suggestion': true}, {'id': 1, 'rate': 12.0, 'suggestion': false}, @@ -212,8 +141,6 @@ describe('oxxionRtdProvider', () => { auctionEnd.bidsReceived = bids; it('call everything', function() { oxxionSubmodule.getBidRequestData(request, null, moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[0], moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[1], moduleConfig); }); it('check bid filtering', function() { let requestsList = oxxionSubmodule.getRequestsList(request); @@ -229,27 +156,5 @@ describe('oxxionRtdProvider', () => { expect(filteredBiddderRequests[1]).to.have.property('bids'); expect(filteredBiddderRequests[1].bids.length).to.equal(1); }); - it('check vastImpUrl', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastImpUrl'); - let expectVastImpUrl = 'https://' + moduleConfig.params.domain + '.oxxion.io/analytics/vast_imp?'; - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(expectVastImpUrl); - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('https://some.tracking-url.com')); - }); - it('check vastXml', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastXml'); - let vastWrapper = new DOMParser().parseFromString(auctionEnd.bidsReceived[0].vastXml, 'text/xml'); - let impressions = vastWrapper.querySelectorAll('VAST Ad Wrapper Impression'); - expect(impressions.length).to.equal(2); - expect(auctionEnd.bidsReceived[1]).to.have.property('vastXml'); - expect(auctionEnd.bidsReceived[1].adId).to.equal('4b2e1581c0ca1a'); - let vastInline = new DOMParser().parseFromString(auctionEnd.bidsReceived[1].vastXml, 'text/xml'); - let inline = vastInline.querySelectorAll('VAST Ad InLine'); - expect(inline).to.have.lengthOf(1); - let inlineImpressions = vastInline.querySelectorAll('VAST Ad InLine Impression'); - expect(inlineImpressions).to.have.lengthOf.above(0); - }); - it('check cpmIncrement', function() { - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('cpmIncrement=0')); - }); }); }); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index 64b345c5d9c..73df2fba8fd 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -2069,6 +2069,19 @@ describe('ozone Adapter', function () { expect(request.length).to.equal(3); config.resetConfig(); }); + it('should not batch into 10s if config is set to false and singleRequest is true', function () { + config.setConfig({ozone: {'batchRequests': false, 'singleRequest': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 15; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); + config.resetConfig(); + }); it('should use GET values auction=dev & cookiesync=dev if set', function() { var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { diff --git a/test/spec/modules/pangleBidAdapter_spec.js b/test/spec/modules/pangleBidAdapter_spec.js new file mode 100644 index 00000000000..6d8f9a66bcf --- /dev/null +++ b/test/spec/modules/pangleBidAdapter_spec.js @@ -0,0 +1,387 @@ +import { expect } from 'chai'; +import { spec } from 'modules/pangleBidAdapter.js'; + +const REQUEST = [{ + adUnitCode: 'adUnitCode1', + bidId: 'bidId1', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}, +{ + adUnitCode: 'adUnitCode2', + bidId: 'bidId2', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}]; + +const DEFAULT_OPTIONS = { + userId: { + britepoolid: 'pangle-britepool', + criteoId: 'pangle-criteo', + digitrustid: { data: { id: 'pangle-digitrust' } }, + id5id: { uid: 'pangle-id5' }, + idl_env: 'pangle-idl-env', + lipb: { lipbid: 'pangle-liveintent' }, + netId: 'pangle-netid', + parrableId: { eid: 'pangle-parrable' }, + pubcid: 'pangle-pubcid', + tdid: 'pangle-ttd', + } +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'requestId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 1, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'pangle': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'seat' + } + ] + } +}; + +describe('pangle bid adapter', function () { + describe('isBidRequestValid', function () { + it('should accept request if placementid and appid is passed', function () { + let bid = { + bidder: 'pangle', + params: { + token: 'xxx', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('reject requests without params', function () { + let bid = { + bidder: 'pangle', + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('creates request data', function () { + let 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); + }); + }); + + describe('interpretResponse', function () { + it('has bids', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + let bids = spec.interpretResponse(RESPONSE, request); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].id); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + const EMPTY_RESP = Object.assign({}, RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); + }); + + describe('parseUserAgent', function () { + let desktop, mobile, tablet; + beforeEach(function () { + desktop = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'; + mobile = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'; + tablet = 'Apple iPad: Mozilla/5.0 (iPad; CPU OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/605.1.15'; + }); + + it('should return correct device type: tablet', function () { + let deviceType = spec.getDeviceType(tablet); + expect(deviceType).to.equal(5); + }); + + it('should return correct device type: mobile', function () { + let deviceType = spec.getDeviceType(mobile); + expect(deviceType).to.equal(4); + }); + + it('should return correct device type: desktop', function () { + let deviceType = spec.getDeviceType(desktop); + expect(deviceType).to.equal(2); + }); + }); +}); + +describe('Pangle Adapter with video', function() { + const videoBidRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const serverResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 1, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + + describe('Video: buildRequests', function() { + it('should create a POST request for video bid', function() { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].method).to.equal('POST'); + }); + + it('should have a valid URL and payload for an out-stream video bid', function () { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].url).to.equal('https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'); + expect(requests[0].data).to.exist; + }); + }); + + describe('interpretResponse: Video', function () { + it('should get correct bid response', function () { + const request = spec.buildRequests(videoBidRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(serverResponse, request); + expect(interpretedResponse).to.be.an('array'); + const bid = interpretedResponse[0]; + expect(bid).to.exist; + expect(bid.requestId).to.exist; + expect(bid.cpm).to.be.above(0); + expect(bid.ttl).to.exist; + expect(bid.creativeId).to.exist; + if (bid.renderer) { + expect(bid.renderer.render).to.exist; + } + }); + }); +}); + +describe('pangle multi-format ads', function () { + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const multiRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { banner: { sizes: [[300, 250]] }, video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const videoResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 2, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + const bannerResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 1, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + it('should set mediaType to banner', function() { + const request = spec.buildRequests(multiRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(bannerResponse, request); + const bid = interpretedResponse[0]; + expect(bid.mediaType).to.equal('banner'); + }) + it('should set mediaType to video', function() { + const request = spec.buildRequests(multiRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(videoResponse, request); + const bid = interpretedResponse[0]; + expect(bid.mediaType).to.equal('video'); + }) +}); diff --git a/test/spec/modules/acuityAdsBidAdapter_spec.js b/test/spec/modules/pgamsspBidAdapter_spec.js similarity index 96% rename from test/spec/modules/acuityAdsBidAdapter_spec.js rename to test/spec/modules/pgamsspBidAdapter_spec.js index 05c59036ff3..0766219eda8 100644 --- a/test/spec/modules/acuityAdsBidAdapter_spec.js +++ b/test/spec/modules/pgamsspBidAdapter_spec.js @@ -1,11 +1,11 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/acuityAdsBidAdapter'; +import { spec } from '../../../modules/pgamsspBidAdapter.js'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'acuityads' +const bidder = 'pgamssp' -describe('AcuityAdsBidAdapter', function () { +describe('PGAMBidAdapter', function () { const bids = [ { bidId: getUniqueIdentifierStr(), @@ -104,7 +104,7 @@ describe('AcuityAdsBidAdapter', function () { }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + expect(serverRequest.url).to.equal('https://us-east.pgammedia.com/pbjs'); }); it('Returns general data valid', function () { @@ -145,6 +145,7 @@ describe('AcuityAdsBidAdapter', function () { expect(placement.schain).to.be.an('object'); expect(placement.bidfloor).to.exist.and.to.equal(0); expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.an('array'); if (placement.adFormat === BANNER) { expect(placement.sizes).to.be.an('array'); @@ -382,7 +383,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pgammedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') }); it('Should return array of objects with proper sync config , include CCPA', function() { const syncData = spec.getUserSyncs({}, {}, {}, { @@ -393,7 +394,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pgammedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') }); }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index ad6c4318cc7..5b591804b95 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -14,7 +14,6 @@ import {config} from 'src/config.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {server} from 'test/mocks/xhr.js'; -import {createEidsArray} from 'modules/userId/eids.js'; import 'modules/appnexusBidAdapter.js'; // appnexus alias test import 'modules/rubiconBidAdapter.js'; // rubicon alias test import 'src/prebid.js'; // $$PREBID_GLOBAL$$.aliasBidder test @@ -34,11 +33,9 @@ import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; -import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; -import {sandbox} from 'sinon'; import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; -import {activityParams} from '../../../src/activities/activityParams.js'; import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; let CONFIG = { @@ -712,8 +709,8 @@ describe('S2S Adapter', function () { beforeEach(() => { s2sReq = { ...REQUEST, - ortb2Fragments: {global: {source: {tid: 'mock-tid'}}}, - ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})) + ortb2Fragments: {global: {}}, + ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})), }; BID_REQUESTS[0].bids[0].ortb2Imp = {ext: {tid: 'mock-tid'}}; }); @@ -726,15 +723,15 @@ describe('S2S Adapter', function () { it('should not be set when transmitTid is not allowed, with ext.prebid.createtids: false', () => { config.setConfig({ s2sConfig: CONFIG, enableTIDs: false }); const req = makeRequest(); - expect(req.source.tid).to.not.exist; - expect(req.imp[0].ext.tid).to.not.exist; + expect(req.source?.tid).to.not.exist; + expect(req.imp[0].ext?.tid).to.not.exist; expect(req.ext.prebid.createtids).to.equal(false); }); - it('should be picked from FPD otherwise', () => { + it('should be set to auction ID otherwise', () => { config.setConfig({s2sConfig: CONFIG, enableTIDs: true}); const req = makeRequest(); - expect(req.source.tid).to.eql('mock-tid'); + expect(req.source.tid).to.eql(BID_REQUESTS[0].auctionId); expect(req.imp[0].ext.tid).to.eql('mock-tid'); }) }) @@ -2054,7 +2051,7 @@ describe('S2S Adapter', function () { const bidRequests = utils.deepClone(BID_REQUESTS); adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); - const parsedRequestBody = JSON.parse(server.requests[1].requestBody); + const parsedRequestBody = JSON.parse(server.requests.find(req => req.method === 'POST').requestBody); expect(parsedRequestBody.cur).to.deep.equal(['NZ']); }); @@ -2876,6 +2873,15 @@ describe('S2S Adapter', function () { events.emit.restore(); }); + it('triggers BIDDER_ERROR on server error', () => { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(400, {}, {}); + BID_REQUESTS.forEach(bidderRequest => { + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.BIDDER_ERROR, sinon.match({bidderRequest})) + }) + }) + // 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 }); @@ -3416,29 +3422,6 @@ describe('S2S Adapter', function () { }); }); describe('when the response contains ext.prebid.fledge', () => { - let fledgeStub, request, bidderRequests; - - function fledgeHook(next, ...args) { - fledgeStub(...args); - } - - before(() => { - addComponentAuction.before(fledgeHook); - }); - - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) - - beforeEach(function () { - fledgeStub = sinon.stub(); - config.setConfig({CONFIG}); - request = deepClone(REQUEST); - request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); - bidderRequests = deepClone(BID_REQUESTS); - bidderRequests.forEach(req => req.fledgeEnabled = true); - }); - const AU = 'div-gpt-ad-1460505748561-0'; const FLEDGE_RESP = { ext: { @@ -3447,12 +3430,14 @@ describe('S2S Adapter', function () { auctionconfigs: [ { impid: AU, + bidder: 'appnexus', config: { id: 1 } }, { impid: AU, + bidder: 'other', config: { id: 2 } @@ -3463,20 +3448,62 @@ describe('S2S Adapter', function () { } } + let fledgeStub, request, bidderRequests; + + function fledgeHook(next, ...args) { + fledgeStub(...args); + } + + before(() => { + addComponentAuction.before(fledgeHook); + }); + + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) + + beforeEach(function () { + fledgeStub = sinon.stub(); + config.setConfig({CONFIG}); + bidderRequests = deepClone(BID_REQUESTS); + AU + bidderRequests.forEach(req => { + Object.assign(req, { + fledgeEnabled: true, + ortb2: { + fpd: 1 + } + }) + req.bids.forEach(bid => { + Object.assign(bid, { + ortb2Imp: { + fpd: 2 + } + }) + }) + }); + request = deepClone(REQUEST); + request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); + }); + + function expectFledgeCalls() { + const auctionId = bidderRequests[0].auctionId; + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: bidderRequests[0].ortb2, ortb2Imp: bidderRequests[0].bids[0].ortb2Imp}), {id: 1}) + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), {id: 2}) + } + it('calls addComponentAuction alongside addBidResponse', function () { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); expect(addBidResponse.called).to.be.true; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }); it('calls addComponentAuction when there is no bid in the response', () => { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); expect(addBidResponse.called).to.be.false; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }) }); }); @@ -3626,33 +3653,6 @@ describe('S2S Adapter', function () { sinon.assert.calledOnce(logErrorSpy); }); - it('should configure the s2sConfig object with appnexus vendor defaults unless specified by user', function () { - const options = { - accountId: '123', - bidders: ['appnexus'], - defaultVendor: 'appnexus', - timeout: 750 - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', '123'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['appnexus']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' - }); - expect(vendorConfig.syncEndpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' - }); - expect(vendorConfig).to.have.property('timeout', 750); - }); - it('should configure the s2sConfig object with appnexuspsp vendor defaults unless specified by user', function () { const options = { accountId: '123', @@ -3673,7 +3673,10 @@ describe('S2S Adapter', function () { p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }); - expect(vendorConfig.syncEndpoint).to.be.undefined; + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -3758,6 +3761,74 @@ describe('S2S Adapter', function () { }) }); + it('should configure the s2sConfig object with openwrap vendor defaults unless specified by user', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap' + }; + + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); + + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', '1234'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['pubmatic']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }); + expect(vendorConfig).to.have.property('timeout', 500); + }); + + it('should return proper defaults', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + }; + + config.setConfig({ s2sConfig: options }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + 'accountId': '1234', + 'adapter': 'prebidServer', + 'bidders': ['pubmatic'], + 'defaultVendor': 'openwrap', + 'enabled': true, + 'endpoint': { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + 'timeout': 500 + }) + }); + + it('should return default adapterOptions if not set', function () { + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + enabled: true, + timeout: 500, + adapter: 'prebidServer', + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + }) + }); + it('should set adapterOptions', function () { config.setConfig({ s2sConfig: { diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js index db38845c11a..78a1615a02e 100644 --- a/test/spec/modules/precisoBidAdapter_spec.js +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -1,5 +1,5 @@ -import {expect} from 'chai'; -import {spec} from '../../../modules/precisoBidAdapter.js'; +import { expect } from 'chai'; +import { spec } from '../../../modules/precisoBidAdapter.js'; import { config } from '../../../src/config.js'; const DEFAULT_PRICE = 1 @@ -10,17 +10,27 @@ const BIDDER_CODE = 'preciso'; describe('PrecisoAdapter', function () { let bid = { - bidId: '23fhj33i987f', bidder: 'preciso', + mediaTypes: { + banner: { + sizes: [[DEFAULT_BANNER_WIDTH, DEFAULT_BANNER_HEIGHT]] + } + }, params: { host: 'prebid', sourceid: '0', publisherId: '0', - traffic: 'banner', + mediaType: 'banner', region: 'prebid-eu' - } + }, + userId: { + pubcid: '12355454test' + + }, + geo: 'NA', + city: 'Asia,delhi' }; describe('isBidRequestValid', function () { @@ -49,35 +59,37 @@ describe('PrecisoAdapter', 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('id', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); + // expect(data).to.be.an('object'); + + // expect(data).to.have.all.keys('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); + expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); expect(data.coppa).to.be.a('number'); expect(data.language).to.be.a('string'); - expect(data.secure).to.be.within(0, 1); + // expect(data.secure).to.be.within(0, 1); expect(data.host).to.be.a('string'); expect(data.page).to.be.a('string'); - let placement = data['placements'][0]; - expect(placement).to.have.keys('region', 'bidId', 'traffic', 'sizes', 'publisherId'); - expect(placement.bidId).to.equal('23fhj33i987f'); - expect(placement.traffic).to.equal('banner'); - expect(placement.region).to.equal('prebid-eu'); - }); - it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); - let data = serverRequest.data; - expect(data.placements).to.be.an('array').that.is.empty; + + expect(data.city).to.be.a('string'); + expect(data.geo).to.be.a('object'); + // expect(data.userId).to.be.a('string'); + // expect(data.imp).to.be.a('object'); }); + // it('Returns empty data if no valid requests are passed', function () { + /// serverRequest = spec.buildRequests([]); + // let data = serverRequest.data; + // expect(data.imp).to.be.an('array').that.is.empty; + // }); }); - describe('with COPPA', function() { - beforeEach(function() { + describe('with COPPA', function () { + beforeEach(function () { sinon.stub(config, 'getConfig') .withArgs('coppa') .returns(true); }); - afterEach(function() { + afterEach(function () { config.getConfig.restore(); }); @@ -90,7 +102,9 @@ describe('PrecisoAdapter', function () { describe('interpretResponse', function () { it('should get correct bid response', function () { let response = { - id: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + + bidderRequestId: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + seatbid: [ { bid: [ @@ -131,7 +145,7 @@ describe('PrecisoAdapter', function () { }) }) describe('getUserSyncs', function () { - const syncUrl = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=preciso_srl&gdpr=0&gdpr_consent=&us_privacy=&t=4'; + const syncUrl = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=NA&gdpr=0&gdpr_consent=&us_privacy=&t=4'; const syncOptions = { iframeEnabled: true }; diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index e2b8ca38792..a31f07fecef 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -12,7 +12,7 @@ import { isFloorsDataValid, addBidResponseHook, fieldMatchingFunctions, - allowedFields + allowedFields, parseFloorData, normalizeDefault, getFloorDataFromAdUnits, updateAdUnitsForAuction, createFloorsDataForAuction } from 'modules/priceFloors.js'; import * as events from 'src/events.js'; import * as mockGpt from '../integration/faker/googletag.js'; @@ -123,7 +123,7 @@ describe('the price floors module', function () { return { code, mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}}, - bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}] + bids: [{bidder: 'someBidder', adUnitCode: code}, {bidder: 'someOtherBidder', adUnitCode: code}] }; } beforeEach(function() { @@ -143,6 +143,76 @@ describe('the price floors module', function () { getGlobal().bidderSettings = {}; }); + describe('parseFloorData', () => { + it('should accept just a default floor', () => { + const fd = parseFloorData({ + default: 1.23 + }); + expect(getFirstMatchingFloor(fd, {}, {}).matchingFloor).to.eql(1.23); + }); + }); + + describe('getFloorDataFromAdUnits', () => { + let adUnits; + + function setFloorValues(rule) { + adUnits.forEach((au, i) => { + au.floors = { + values: { + [rule]: i + 1 + } + } + }) + } + + beforeEach(() => { + adUnits = ['au1', 'au2', 'au3'].map(getAdUnitMock); + }) + + it('should use one schema for all adUnits', () => { + setFloorValues('*;*') + adUnits[1].floors.schema = { + fields: ['mediaType', 'gptSlot'], + delimiter: ';' + } + sinon.assert.match(getFloorDataFromAdUnits(adUnits), { + schema: { + fields: ['adUnitCode', 'mediaType', 'gptSlot'], + delimiter: ';' + }, + values: { + 'au1;*;*': 1, + 'au2;*;*': 2, + 'au3;*;*': 3 + } + }) + }); + it('should ignore adUnits that declare different schema', () => { + setFloorValues('*|*'); + adUnits[0].floors.schema = { + fields: ['mediaType', 'gptSlot'] + }; + adUnits[2].floors.schema = { + fields: ['gptSlot', 'mediaType'] + }; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*|*': 1, + 'au2|*|*': 2 + }) + }); + it('should ignore adUnits that declare no values', () => { + setFloorValues('*'); + adUnits[0].floors.schema = { + fields: ['mediaType'] + }; + delete adUnits[2].floors.values; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*': 1, + 'au2|*': 2, + }) + }) + }) + describe('getFloorsDataForAuction', function () { it('converts basic input floor data into a floorData map for the auction correctly', function () { // basic input where nothing needs to be updated @@ -233,8 +303,8 @@ describe('the price floors module', function () { }); describe('getFirstMatchingFloor', function () { - it('uses a 0 floor as overrite', function () { - let inputFloorData = { + it('uses a 0 floor as override', function () { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '|', @@ -245,7 +315,7 @@ describe('the price floors module', function () { 'test_div_2': 2 }, default: 0.5 - }; + }); expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ floorMin: 0, @@ -434,7 +504,7 @@ describe('the price floors module', function () { }); }); it('selects the right floor for more complex rules', function () { - let inputFloorData = { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '^', @@ -448,7 +518,7 @@ describe('the price floors module', function () { 'weird_div^*^300x250': 5.5 }, default: 0.5 - }; + }); // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ floorMin: 0, @@ -490,10 +560,8 @@ describe('the price floors module', function () { matchingFloor: undefined }); // if default is there use it - inputFloorData = { default: 5.0 }; - expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ - matchingFloor: 5.0 - }); + inputFloorData = normalizeDefault({ default: 5.0 }); + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'}).matchingFloor).to.equal(5.0); }); describe('with gpt enabled', function () { let gptFloorData; @@ -580,6 +648,90 @@ describe('the price floors module', function () { }); }); }); + + describe('updateAdUnitsForAuction', function() { + let inputFloorData; + let adUnits; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + inputFloorData = utils.deepClone(minFloorConfigLow); + inputFloorData.skipRate = 0.5; + }); + + it('should set the skipRate to the skipRate from the data property before using the skipRate from floorData directly', function() { + utils.deepSetValue(inputFloorData, 'data', { + skipRate: 0.7 + }); + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(0.7); + }); + + it('should set the skipRate to the skipRate from floorData directly if it does not exist in the data property of floorData', function() { + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(0.5); + }); + + it('should set the skipRate in the bid floorData to undefined if both skipRate and skipRate in the data property are undefined', function() { + inputFloorData.skipRate = undefined; + utils.deepSetValue(inputFloorData, 'data', { + skipRate: undefined, + }); + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(undefined); + }); + }); + + describe('createFloorsDataForAuction', function() { + let adUnits; + let floorConfig; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + floorConfig = utils.deepClone(basicFloorConfig); + }); + + it('should return skipRate as 0 if both skipRate and skipRate in the data property are undefined', function() { + floorConfig.skipRate = undefined; + floorConfig.data.skipRate = undefined; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.skipRate).to.equal(0); + expect(floorData.skipped).to.equal(false); + }); + + it('should properly set skipRate if it is available in the data property', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + floorConfig.data.skipRate = 201; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.data.skipRate).to.equal(201); + expect(floorData.skipped).to.equal(true); + }); + + it('should should use the skipRate if its not available in the data property ', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.skipRate).to.equal(101); + expect(floorData.skipped).to.equal(true); + }); + }); + describe('pre-auction tests', function () { let exposedAdUnits; const validateBidRequests = (getFloorExpected, FloorDataExpected) => { @@ -621,6 +773,124 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + it('should not do floor stuff if floors.data is defined by noFloorSignalBidders[]', function() { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + }}); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('should not do floor stuff if floors.enforcement is defined by noFloorSignalBidders[]', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + }, + data: basicFloorDataLow + }); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('should not do floor stuff and use first floors.data.noFloorSignalBidders if its defined betwen enforcement.noFloorSignalBidders', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + noFloorSignalBidders: ['someBidder'] + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + } + }); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('it shouldn`t return floor stuff for bidder in the noFloorSignalBidders list', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder'] + } + }); + runStandardAuction() + const bidRequestData = exposedAdUnits[0].bids.find(bid => bid.bidder === 'someBidder'); + expect(bidRequestData.hasOwnProperty('getFloor')).to.equal(false); + sinon.assert.match(bidRequestData.floorData, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }); + }) + it('it should return floor stuff if we defined wrong bidder name in data.noFloorSignalBidders', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['randomBiider'] + } + }); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: false + }) + }); it('should use adUnit level data if not setConfig or fetch has occured', function () { handleSetFloorsConfig({ ...basicFloorConfig, @@ -693,6 +963,95 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + describe('default floor', () => { + let adUnits; + beforeEach(() => { + adUnits = ['au1', 'au2'].map(getAdUnitMock); + }) + function expectFloors(floors) { + runStandardAuction(adUnits); + adUnits.forEach((au, i) => { + au.bids.forEach(bid => { + expect(bid.getFloor().floor).to.eql(floors[i]); + }) + }) + } + describe('should be sufficient by itself', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + default: 1.23 + } + }); + expectFloors([1.23, 1.23]) + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = {default: 1}; + adUnits[1].floors = {default: 2}; + expectFloors([1, 2]) + }); + it('on an adUnit with hidden schema', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + default: 1 + } + adUnits[1].floors = { + default: 2 + } + expectFloors([1, 2]); + }) + }); + describe('should NOT be used when a star rule exists', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 2 + }, + default: 3, + } + }); + expectFloors([2, 2]); + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 1 + }, + default: 3 + }; + adUnits[1].floors = { + values: { + '*|*': 2 + }, + default: 4 + } + expectFloors([1, 2]); + }) + }); + }) it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'}); runStandardAuction(); @@ -1382,7 +1741,7 @@ describe('the price floors module', function () { it('picks the right rule with more complex rules', function () { _floorDataForAuction[bidRequest.auctionId] = { ...basicFloorConfig, - data: { + data: normalizeDefault({ currency: 'USD', schema: { fields: ['mediaType', 'size'], delimiter: '|' }, values: { @@ -1394,7 +1753,7 @@ describe('the price floors module', function () { 'video|*': 5.5 }, default: 10.0 - } + }) }; // assumes banner * @@ -1854,6 +2213,12 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); expect(logWarnSpy.calledOnce).to.equal(true); }); + it('if it finds a rule with a floor price of zero it should not call log warn', function () { + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { '*': 0 }; + runBidResponse(); + expect(logWarnSpy.calledOnce).to.equal(false); + }); it('if it finds a rule and floors should update the bid accordingly', function () { _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; diff --git a/test/spec/modules/programmaticaBidAdapter_spec.js b/test/spec/modules/programmaticaBidAdapter_spec.js new file mode 100644 index 00000000000..247d20752c3 --- /dev/null +++ b/test/spec/modules/programmaticaBidAdapter_spec.js @@ -0,0 +1,263 @@ +import { expect } from 'chai'; +import { spec } from 'modules/programmaticaBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('programmaticaBidAdapterTests', function () { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + } + }) + ).to.equal(true); + }); + + it('validate_generated_url', function () { + const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 }); + let req_url = request[0].url; + + expect(req_url).to.equal('https://asr.programmatica.com/get'); + }); + + it('validate_response_params', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': null, + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }); + + it('validate_response_params_imps', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': [ + 'testImp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }) + + it('validate_invalid_response', function () { + let serverResponse = { + body: {} + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(0); + }) + + it('video_bid', function () { + const bidRequest = deepClone(bidRequestData.bids); + bidRequest[0].mediaTypes = { + video: { + playerSize: [234, 765] + } + }; + + const request = spec.buildRequests(bidRequest, { timeout: 1234 }); + const vastXml = ''; + let serverResponse = { + body: { + 'id': 'cki2n3n6snkuulqutpf0', + 'type': { + 'format': '', + 'source': 'rtb', + 'dspId': '1' + }, + 'content': { + 'data': vastXml, + 'imps': [ + 'https://asr.dev.programmatica.com/track/imp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '', + 'matching': '', + 'cpm': 70, + 'currency': 'RUB' + } + }; + + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(vastXml); + expect(bid.width).to.equal(234); + expect(bid.height).to.equal(765); + }); +}); + +describe('getUserSyncs', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({ + pixelEnabled: true, + }, {}, {}, '1---'); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp?usp=1---&consent=') + }); + + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: true + }, + }, + } + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1') + }); + + it('Should return array of objects with proper sync config , include GDPR, no purpose', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: false + }, + }, + } + }, ''); + expect(syncData).is.empty; + }); + + it('Should return array of objects with proper sync config , GDPR not applies', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: false, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0') + }); +}) diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index f35a7453403..5ad58ea1a37 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -72,11 +72,6 @@ describe('PublinkIdSystem', () => { expect(result.callback).to.be.a('function'); }); - it('Use local copy', () => { - const result = publinkIdSubmodule.getId({}, undefined, TEST_COOKIE_VALUE); - expect(result).to.be.undefined; - }); - describe('callout for id', () => { let callbackSpy = sinon.spy(); @@ -84,6 +79,44 @@ describe('PublinkIdSystem', () => { callbackSpy.resetHistory(); }); + it('Has cached id', () => { + const config = {storage: {type: 'cookie'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink/refresh'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + + it('Request path has priority', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + it('Fetch with consent data', () => { const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; const consentData = {gdprApplies: 1, consentString: 'myconsentstring'}; diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index 1b2e80aa730..c6447905ecd 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -1,10 +1,11 @@ -import pubmaticAnalyticsAdapter, {getMetadata} from 'modules/pubmaticAnalyticsAdapter.js'; +import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; -import {config} from 'src/config.js'; -import {setConfig} from 'modules/currency.js'; -import {server} from '../../mocks/xhr.js'; +import { config } from 'src/config.js'; +import { setConfig } from 'modules/currency.js'; +import { server } from '../../mocks/xhr.js'; import 'src/prebid.js'; +import { getGlobal } from 'src/prebidGlobal'; let events = require('src/events'); let ajax = require('src/ajax'); @@ -315,6 +316,208 @@ describe('pubmatic analytics adapter', function () { expect(utils.logError.called).to.equal(true); }); + describe('OW S2S', function() { + this.beforeEach(function() { + pubmaticAnalyticsAdapter.enableAnalytics({ + options: { + publisherId: 9999, + profileId: 1111, + profileVersionId: 20 + } + }); + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + }); + + this.afterEach(function() { + pubmaticAnalyticsAdapter.disableAnalytics(); + }); + + it('Pubmatic Won: No tracker fired', function() { + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(1); // only logger is fired + let request = requests[0]; + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.pubid).to.equal('9999'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + }); + + it('Non-pubmatic won: logger, tracker fired', function() { + const APPNEXUS_BID = Object.assign({}, BID, { + 'bidder': 'appnexus', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '2ecff0db240757', + 'hb_pb': 1.20, + 'hb_size': '640x480', + 'hb_source': 'server' + } + }); + + const MOCK_AUCTION_INIT_APPNEXUS = { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'appnexus', + 'params': { + 'publisherId': '1001' + } + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } + ], + 'adUnitCodes': ['/19968336/header-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'appnexus', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'appnexus', + 'params': { + 'publisherId': '1001', + 'kgpv': 'this-is-a-kgpv' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 + }; + + const MOCK_BID_REQUESTED_APPNEXUS = { + 'bidder': 'appnexus', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'appnexus', + 'adapterCode': 'appnexus', + 'bidderCode': 'appnexus', + 'params': { + 'publisherId': '1001', + 'video': { + 'minduration': 30, + 'skippable': true + } + }, + 'mediaType': 'video', + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + } + ], + 'auctionStart': 1519149536560, + 'timeout': 5000, + 'start': 1519149562216, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + }, + 'gdprConsent': { + 'consentString': 'here-goes-gdpr-consent-string', + 'gdprApplies': true + } + }; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [APPNEXUS_BID] + }); + + events.emit(AUCTION_INIT, MOCK_AUCTION_INIT_APPNEXUS); + events.emit(BID_REQUESTED, MOCK_BID_REQUESTED_APPNEXUS); + events.emit(BID_RESPONSE, APPNEXUS_BID); + events.emit(BIDDER_DONE, { + 'bidderCode': 'appnexus', + 'bids': [ + APPNEXUS_BID, + Object.assign({}, APPNEXUS_BID, { + 'serverResponseTimeMs': 42, + }) + ] + }); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, { + [APPNEXUS_BID.adUnitCode]: APPNEXUS_BID.adserverTargeting, + }); + events.emit(BID_WON, Object.assign({}, APPNEXUS_BID, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // logger as well as tracker is fired + let request = requests[1]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.pubid).to.equal('9999'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + + let firstTracker = requests[0].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.pubid).to.equal('9999'); + expect(decodeURIComponent(data.purl)).to.equal('http://www.test.com/page.html'); + + expect(data.s).to.be.an('array'); + expect(data.s.length).to.equal(1); + expect(data.s[0].ps[0].pn).to.equal('appnexus'); + expect(data.s[0].ps[0].bc).to.equal('appnexus'); + }) + }); + describe('when handling events', function() { beforeEach(function () { pubmaticAnalyticsAdapter.enableAnalytics({ @@ -367,15 +570,20 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].sid).not.to.be.undefined; + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); - expect(data.s[0].ps.length).to.equal(1); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -386,9 +594,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(1); expect(data.s[0].ps[0].t).to.equal(0); @@ -400,6 +609,10 @@ describe('pubmatic analytics adapter', function () { // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -417,7 +630,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -455,7 +669,7 @@ describe('pubmatic analytics adapter', function () { expect(data.af).to.equal('video'); }); - it('Logger: do not log floor fields when prebids floor shows noData in location property', function() { + it('Logger : do not log floor fields when prebids floor shows noData in location property', function() { const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'noData'; @@ -572,15 +786,20 @@ describe('pubmatic analytics adapter', function () { expect(data.pid).to.equal('1111'); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); expect(data.tgid).to.equal(0); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].sid).not.to.be.undefined; + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); @@ -646,6 +865,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be between 0-15 else set to 0 expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -653,7 +873,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -696,6 +916,13 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be an INT between 0-15 else set to 0 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + + expect(data.s[1].sid).not.to.be.undefined; + + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -708,7 +935,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -746,7 +973,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -778,6 +1005,10 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -794,7 +1025,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(0); + expect(data.s[0].ps[0].ol1).to.equal(0); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(1); @@ -839,6 +1071,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); expect(data.s[1].ps[0].bc).to.equal('pubmatic'); @@ -853,7 +1086,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -885,6 +1119,10 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -901,7 +1139,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -943,6 +1182,7 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -958,7 +1198,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -996,6 +1237,10 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -1012,7 +1257,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -1052,6 +1298,7 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -1067,7 +1314,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -1109,7 +1357,11 @@ describe('pubmatic analytics adapter', function () { // Testing only for rejected bid as other scenarios will be covered under other TCs expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -1126,7 +1378,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -1175,6 +1428,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1182,9 +1436,13 @@ describe('pubmatic analytics adapter', function () { // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic_alias'); @@ -1196,9 +1454,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1211,7 +1470,11 @@ describe('pubmatic analytics adapter', function () { // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -1228,7 +1491,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -1294,6 +1558,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1301,9 +1566,13 @@ describe('pubmatic analytics adapter', function () { // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('groupm'); @@ -1315,9 +1584,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1330,6 +1600,7 @@ describe('pubmatic analytics adapter', function () { // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -1346,7 +1617,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 0ed764c8f7e..5d59ff99a89 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -82,6 +82,7 @@ describe('PubMatic adapter', function () { ortb2Imp: { ext: { tid: '92489f71-1bf2-49a0-adf9-000cea934729', + gpid: '/1111/homepage-leftnav' } }, schain: schainConfig @@ -103,6 +104,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5890', adSlot: 'Div1@0x0', // ad_id or tagid + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -153,6 +155,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5890', adSlot: 'Div1@640x480', // ad_id or tagid + wiid: '1234567890', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -212,6 +215,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' }, bidId: '2a5571261281d4', requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', @@ -277,6 +281,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' }, bidId: '2a5571261281d4', requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', @@ -303,6 +308,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' } }]; @@ -343,6 +349,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' } }]; @@ -501,6 +508,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '301', adSlot: '/15671365/DMDemo@300x250:0', + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -571,6 +579,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '301', adSlot: '/15671365/DMDemo@300x250:0', + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -1172,6 +1181,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1439,6 +1449,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].banner.w).to.equal(728); // width expect(data.imp[0].banner.h).to.equal(90); // height expect(data.imp[0].banner.format).to.deep.equal([{w: 160, h: 600}]); + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1663,6 +1674,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid }); @@ -1711,6 +1723,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid @@ -1759,6 +1772,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid // second request without USP/CCPA @@ -1908,7 +1922,43 @@ describe('PubMatic adapter', function () { expect(data.user.yob).to.equal(1985); }); + it('ortb2.badv should be merged in the request', function() { + const ortb2 = { + badv: ['example.com'] + }; + const request = spec.buildRequests(bidRequests, {ortb2}); + let data = JSON.parse(request.data); + expect(data.badv).to.deep.equal(['example.com']); + }); + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.gpid', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should send gpid if imp[].ext.gpid is specified', function() { + bidRequests[0].ortb2Imp = { + ext: { + gpid: 'ortb2Imp.ext.gpid' + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.have.property('gpid'); + expect(data.imp[0].ext.gpid).to.equal('ortb2Imp.ext.gpid'); + }); + + it('should not send if imp[].ext.gpid is not specified', function() { + bidRequests[0].ortb2Imp = { ext: { } }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('gpid'); + }); + }); + describe('ortb2Imp.ext.data.pbadslot', function() { beforeEach(function () { if (bidRequests[0].hasOwnProperty('ortb2Imp')) { @@ -2278,6 +2328,23 @@ describe('PubMatic adapter', function () { expect(data.device.sua).to.deep.equal(suaObject); }); + it('should pass device.ext.cdep if present in bidderRequest fpd ortb2 object', function () { + const cdepObj = { + cdep: 'example_label_1' + }; + let request = spec.buildRequests(multipleMediaRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: cdepObj + } + } + }); + let data = JSON.parse(request.data); + expect(data.device.ext.cdep).to.exist.and.to.be.an('string'); + expect(data.device.ext).to.deep.equal(cdepObj); + }); + it('Request params should have valid native bid request for all valid params', function () { let request = spec.buildRequests(nativeBidRequests, { auctionId: 'new-auction-id' @@ -3601,6 +3668,89 @@ describe('PubMatic adapter', function () { } }); + describe('Fledge Auction config Response', function () { + let response; + let bidRequestConfigs = [ + { + bidder: 'pubmatic', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + params: { + publisherId: '5670', + adSlot: '/15671365/DMDemo@300x250:0', + kadfloor: '1.2', + pmzoneid: 'aabc, ddef', + kadpageurl: 'www.publisher.com', + yob: '1986', + gender: 'M', + lat: '12.3', + lon: '23.7', + wiid: '1234567890', + profId: '100', + verId: '200', + currency: 'AUD', + dctr: 'key1:val1,val2|key2:val1' + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: 'test_bid_id', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + ortb2Imp: { + ext: { + tid: '92489f71-1bf2-49a0-adf9-000cea934729', + ae: 1 + } + }, + } + ]; + + let bidRequest = spec.buildRequests(bidRequestConfigs, {}); + let bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test_bid_id', + price: 2, + w: 728, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS', + ext: { + fledge_auction_configs: { + 'test_bid_id': { + seller: 'ads.pubmatic.com', + interestGroupBuyers: ['dsp1.com'], + sellerTimeout: 0, + perBuyerSignals: { + 'dsp1.com': { + bid_macros: 0.1, + disallowed_adv_ids: [ + '5678', + '5890' + ], + } + } + } + } + } + }; + + response = spec.interpretResponse({ body: bidResponse }, bidRequest); + it('should return FLEDGE auction_configs alongside bids', function () { + expect(response).to.have.property('bids'); + expect(response).to.have.property('fledgeAuctionConfigs'); + expect(response.fledgeAuctionConfigs.length).to.equal(1); + expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test_bid_id'); + }); + }); + describe('Preapare metadata', function () { it('Should copy all fields from ext to meta', function () { const bid = { diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 1dce87e4b8e..e0f4497a8c8 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -1,13 +1,9 @@ -import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; -import { getDeviceType, getBrowser, getOS } from 'modules/pubxaiAnalyticsAdapter.js'; -import { - expect -} from 'chai'; +import pubxaiAnalyticsAdapter, {getBrowser, getDeviceType, getOS} from 'modules/pubxaiAnalyticsAdapter.js'; +import {expect} from 'chai'; import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { - server -} from 'test/mocks/xhr.js'; +import {server} from 'test/mocks/xhr.js'; +import {getGptSlotInfoForAdUnitCode} from '../../../libraries/gptUtils/gptUtils.js'; let events = require('src/events'); let constants = require('src/constants.json'); @@ -527,7 +523,7 @@ describe('pubxai analytics adapter', function() { 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'sizes': '300x250', 'renderStatus': 2, @@ -596,7 +592,7 @@ describe('pubxai analytics adapter', function() { let expectedAfterBidWon = { 'winningBid': { 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', diff --git a/test/spec/modules/qortexRtdProvider_spec.js b/test/spec/modules/qortexRtdProvider_spec.js new file mode 100644 index 00000000000..9baa526e4cc --- /dev/null +++ b/test/spec/modules/qortexRtdProvider_spec.js @@ -0,0 +1,333 @@ +import * as utils from 'src/utils'; +import * as ajax from 'src/ajax.js'; +import * as events from 'src/events.js'; +import CONSTANTS from '../../../src/constants.json'; +import {loadExternalScript} from 'src/adloader.js'; +import { + qortexSubmodule as module, + getContext, + addContextToRequests, + setContextData, + initializeModuleData, + loadScriptTag +} from '../../../modules/qortexRtdProvider'; +import {server} from '../../mocks/xhr.js'; +import { cloneDeep } from 'lodash'; + +describe('qortexRtdProvider', () => { + let logWarnSpy; + let ortb2Stub; + + const defaultApiHost = 'https://demand.qortex.ai'; + const defaultGroupId = 'test'; + + const validBidderArray = ['qortex', 'test']; + const validTagConfig = { + videoContainer: 'my-video-container' + } + + const validModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray + } + }, + emptyModuleConfig = { + params: {} + } + + const validImpressionEvent = { + detail: { + uid: 'uid123', + type: 'qx-impression' + } + }, + validImpressionEvent2 = { + detail: { + uid: 'uid1234', + type: 'qx-impression' + } + }, + missingIdImpressionEvent = { + detail: { + type: 'qx-impression' + } + }, + invalidTypeQortexEvent = { + detail: { + type: 'invalid-type' + } + } + + const responseHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*' + }; + + const responseObj = { + content: { + id: '123456', + episode: 15, + title: 'test episode', + series: 'test show', + season: '1', + url: 'https://example.com/file.mp4' + } + }; + + const apiResponse = JSON.stringify(responseObj); + + const reqBidsConfig = { + adUnits: [{ + bids: [ + { bidder: 'qortex' } + ] + }], + ortb2Fragments: { + bidder: {}, + global: {} + } + } + + beforeEach(() => { + ortb2Stub = sinon.stub(reqBidsConfig, 'ortb2Fragments').value({bidder: {}, global: {}}) + logWarnSpy = sinon.spy(utils, 'logWarn'); + }) + + afterEach(() => { + logWarnSpy.restore(); + ortb2Stub.restore(); + setContextData(null); + }) + + describe('init', () => { + it('returns true for valid config object', () => { + expect(module.init(validModuleConfig)).to.be.true; + }) + + it('returns false and logs error for missing groupId', () => { + expect(module.init(emptyModuleConfig)).to.be.false; + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('Qortex RTD module config does not contain valid groupId parameter. Config params: {}')).to.be.ok; + }) + + it('loads Qortex script if tagConfig is present in module config params', () => { + const config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + expect(module.init(config)).to.be.true; + expect(loadExternalScript.calledOnce).to.be.true; + }) + }) + + describe('loadScriptTag', () => { + let addEventListenerSpy; + let billableEvents = []; + + let config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, (e) => { + billableEvents.push(e); + }) + + beforeEach(() => { + initializeModuleData(config); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }) + + afterEach(() => { + addEventListenerSpy.restore(); + billableEvents = []; + }) + + it('adds event listener', () => { + loadScriptTag(config); + expect(addEventListenerSpy.calledOnce).to.be.true; + }) + + it('parses incoming qortex-impression events', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(billableEvents[0].type).to.be.equal(validImpressionEvent.detail.type); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + }) + + it('will emit two events for impressions with two different ids', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent2)); + expect(billableEvents.length).to.be.equal(2); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + expect(billableEvents[1].transactionId).to.be.equal(validImpressionEvent2.detail.uid); + }) + + it('will not allow multiple events with the same id', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(logWarnSpy.calledWith('received invalid billable event due to duplicate uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with missing uid', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', missingIdImpressionEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event due to missing uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with unavailable type', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', invalidTypeQortexEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event: invalid-type')).to.be.ok; + }) + }) + + describe('getBidRequestData', () => { + let callbackSpy; + + beforeEach(() => { + initializeModuleData(validModuleConfig); + callbackSpy = sinon.spy(); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + callbackSpy.resetHistory(); + }) + + it('will call callback immediately if no adunits', () => { + const reqBidsConfigNoBids = { adUnits: [] }; + module.getBidRequestData(reqBidsConfigNoBids, callbackSpy); + expect(callbackSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfigNoBids))).to.be.ok; + }) + + it('will call callback if getContext does not throw', () => { + const cb = function () { + expect(logWarnSpy.calledOnce).to.be.false; + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + server.requests[0].respond(200, responseHeaders, apiResponse); + }) + + it('will catch and log error and fire callback', (done) => { + const a = sinon.stub(ajax, 'ajax').throws(new Error('test')); + const cb = function () { + expect(logWarnSpy.calledWith('test')).to.be.eql(true); + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + a.restore(); + }) + }) + + describe('getContext', () => { + beforeEach(() => { + initializeModuleData(validModuleConfig); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + }) + + it('returns a promise', (done) => { + const result = getContext(); + expect(result).to.be.a('promise'); + done(); + }) + + it('uses request url generated from initialize function in config and resolves to content object data', (done) => { + let requestUrl = `${validModuleConfig.params.apiUrl}/api/v1/analyze/${validModuleConfig.params.groupId}/prebid`; + const ctx = getContext() + expect(server.requests.length).to.be.eql(1); + expect(server.requests[0].url).to.be.eql(requestUrl); + server.requests[0].respond(200, responseHeaders, apiResponse); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('will return existing context data instead of ajax call if the source was not updated', (done) => { + setContextData(responseObj.content); + const ctx = getContext(); + expect(server.requests.length).to.be.eql(0); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('returns null for non erroring api responses other than 200', (done) => { + const nullContentResponse = { content: null } + const ctx = getContext() + server.requests[0].respond(200, responseHeaders, JSON.stringify(nullContentResponse)) + ctx.then(response => { + expect(response).to.be.null; + expect(server.requests.length).to.be.eql(1); + expect(logWarnSpy.called).to.be.false; + done(); + }); + }) + }) + + describe(' addContextToRequests', () => { + it('logs error if no data was retrieved from get context call', () => { + initializeModuleData(validModuleConfig); + addContextToRequests(reqBidsConfig); + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No context data received at this time')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to global ortb2 when bidders array is omitted', () => { + const omittedBidderArrayConfig = cloneDeep(validModuleConfig); + delete omittedBidderArrayConfig.params.bidders; + initializeModuleData(omittedBidderArrayConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + expect(reqBidsConfig.ortb2Fragments.global).to.have.property('site'); + expect(reqBidsConfig.ortb2Fragments.global.site).to.have.property('content'); + expect(reqBidsConfig.ortb2Fragments.global.site.content).to.be.eql(responseObj.content); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to bidder ortb2 when bidders array is included', () => { + initializeModuleData(validModuleConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + + const qortexOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['qortex'] + expect(qortexOrtb2Fragment).to.not.be.null; + expect(qortexOrtb2Fragment).to.have.property('site'); + expect(qortexOrtb2Fragment.site).to.have.property('content'); + expect(qortexOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + const testOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['test'] + expect(testOrtb2Fragment).to.not.be.null; + expect(testOrtb2Fragment).to.have.property('site'); + expect(testOrtb2Fragment.site).to.have.property('content'); + expect(testOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + }) + + it('logs error if there is an empty bidder array', () => { + const invalidBidderArrayConfig = cloneDeep(validModuleConfig); + invalidBidderArrayConfig.params.bidders = []; + initializeModuleData(invalidBidderArrayConfig); + setContextData(responseObj.content) + addContextToRequests(reqBidsConfig); + + expect(logWarnSpy.calledWith('Config contains an empty bidders array, unable to determine which bids to enrich')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + }) +}) diff --git a/test/spec/modules/r2b2BidAdapter_spec.js b/test/spec/modules/r2b2BidAdapter_spec.js new file mode 100644 index 00000000000..b94b400a71d --- /dev/null +++ b/test/spec/modules/r2b2BidAdapter_spec.js @@ -0,0 +1,689 @@ +import {expect} from 'chai'; +import {spec, internal as r2b2, internal} from 'modules/r2b2BidAdapter.js'; +import * as utils from '../../../src/utils'; +import 'modules/schain.js'; +import 'modules/userId/index.js'; + +function encodePlacementIds (ids) { + return btoa(JSON.stringify(ids)); +} + +describe('R2B2 adapter', function () { + let serverResponse, requestForInterpretResponse; + let bidderRequest; + let bids = []; + let gdprConsent = { + gdprApplies: true, + consentString: 'consent-string', + }; + let schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }; + const usPrivacyString = '1YNN'; + const impId = 'impID'; + const price = 10.6; + const ad = 'adm'; + const creativeId = 'creativeID'; + const cid = 41849; + const cdid = 595121; + const unitCode = 'unitCode'; + const bidId1 = '1'; + const bidId2 = '2'; + const bidId3 = '3'; + const bidId4 = '4'; + const bidId5 = '5'; + const bidWonUrl = 'url1'; + const setTargetingUrl = 'url2'; + const bidder = 'r2b2'; + const foreignBidder = 'differentBidder'; + const id1 = { pid: 'd/g/p' }; + const id1Object = { d: 'd', g: 'g', p: 'p', m: 0 }; + const id2 = { pid: 'd/g/p/1' }; + const id2Object = { d: 'd', g: 'g', p: 'p', m: 1 }; + const badId = { pid: 'd/g/' }; + const bid1 = { bidId: bidId1, bidder, params: [ id1 ] }; + const bid2 = { bidId: bidId2, bidder, params: [ id2 ] }; + const bidWithBadSetup = { bidId: bidId3, bidder, params: [ badId ] }; + const bidForeign1 = { bidId: bidId4, bidder: foreignBidder, params: [ { id: 'abc' } ] }; + const bidForeign2 = { bidId: bidId5, bidder: foreignBidder, params: [ { id: 'xyz' } ] }; + const fakeTime = 1234567890; + const cacheBusterRegex = /[\?&]cb=([^&]+)/; + let bidStub, time; + + beforeEach(function () { + bids = [{ + bidder: 'r2b2', + params: { + pid: 'example.com/generic/300x250/1' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + adUnitCode: unitCode, + transactionId: '29c408b9-65ce-48b1-9167-18a57791f908', + sizes: [ + [300, 250] + ], + bidId: '20917a54ee9858', + bidderRequestId: '15270d403778d', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + src: 'client', + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + schain + }, { + bidder: 'r2b2', + params: { + pid: 'example.com/generic/300x600/0' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + adUnitCode: unitCode, + transactionId: '29c408b9-65ce-48b1-9167-18a57791f908', + sizes: [ + [300, 600] + ], + bidId: '3dd53d30c691fe', + bidderRequestId: '15270d403778d', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + src: 'client', + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + schain + }]; + bidderRequest = { + bidderCode: 'r2b2', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + bidderRequestId: '15270d403778d', + bids: bids, + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + gdprConsent: { + consentString: 'consent-string', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }, + uspConsent: '1YYY', + }; + serverResponse = { + id: 'a66a6e32-2a7d-4ed3-bb13-6f3c9bdcf6a1', + seatbid: [{ + bid: [{ + id: '4756cc9e9b504fd0bd39fdd594506545', + impid: impId, + price: price, + adm: ad, + crid: creativeId, + w: 300, + h: 250, + ext: { + prebid: { + meta: { + adaptercode: 'r2b2' + }, + type: 'banner' + }, + r2b2: { + cdid: cdid, + cid: cid, + useRenderer: true + } + } + }], + seat: 'seat' + }] + }; + requestForInterpretResponse = { + data: { + imp: [ + {id: impId} + ] + }, + bids + }; + }); + + describe('isBidRequestValid', function () { + let bid = {}; + + it('should return false when missing required "pid" param', function () { + bid.params = {random: 'param'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {d: 'd', g: 'g', p: 'p', m: 1}; + expect(spec.isBidRequestValid(bid)).to.equal(false) + }); + + it('should return false when "pid" is malformed', function () { + bid.params = {pid: 'pid'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: '///'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: '/g/p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd//p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/g//m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/p/'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/g/p/m/t'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when "pid" is a correct dgpm', function () { + bid.params = {pid: 'd/g/p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when type is blank', function () { + bid.params = {pid: 'd/g/p/'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when type is missing', function () { + bid.params = {pid: 'd/g/p'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when "pid" is a number', function () { + bid.params = {pid: 12356}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when "pid" is a numeric string', function () { + bid.params = {pid: '12356'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true for selfpromo unit', function () { + bid.params = {pid: 'selfpromo'}; + expect(spec.isBidRequestValid(bid)).to.equal(true) + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + r2b2.placementsToSync = []; + r2b2.mappedParams = {}; + }); + + it('should set correct request method and url and pass bids', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let request = requests[0] + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://hb.r2b2.cz/openrtb2/bid'); + expect(request.data).to.be.an('object'); + expect(request.bids).to.deep.equal(bids); + }); + + it('should pass correct parameters', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp, device, site, source, ext, cur, test} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(device).to.be.an('object'); + expect(site).to.be.an('object'); + expect(source).to.be.an('object'); + expect(cur).to.deep.equal(['USD']); + expect(ext.version).to.equal('1.0.0'); + expect(test).to.equal(0); + }); + + it('should pass correct imp', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.id).to.equal('20917a54ee9858'); + expect(bid.banner).to.deep.equal({topframe: 0, format: [{w: 300, h: 250}]}); + expect(bid.ext).to.be.an('object'); + expect(bid.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1}); + }); + + it('should map type correctly', function () { + let result, bid; + let requestWithId = function(id) { + let b = bids[0]; + b.params.pid = id; + let passedBids = [b]; + bidderRequest.bids = passedBids; + return spec.buildRequests(passedBids, bidderRequest); + }; + + result = requestWithId('example.com/generic/300x250/mobile'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250/desktop'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + + result = requestWithId('example.com/generic/300x250/1'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250/0'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + + result = requestWithId('example.com/generic/300x250/m'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + }); + + it('should pass correct parameters for test ad', function () { + let testAdBid = bids[0]; + testAdBid.params = {pid: 'selfpromo'}; + let requests = spec.buildRequests([testAdBid], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.ext).to.be.an('object'); + expect(bid.ext.r2b2).to.deep.equal({d: 'test', g: 'test', p: 'selfpromo', m: 0, 'selfpromo': 1}); + }); + + it('should pass multiple bids', function () { + let requests = spec.buildRequests(bids, bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(bids.length); + let bid1 = imp[0]; + expect(bid1.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1}); + let bid2 = imp[1]; + expect(bid2.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x600', m: 0}); + }); + + it('should set up internal variables', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let bid1Id = bids[0].bidId; + let bid2Id = bids[1].bidId; + expect(r2b2.placementsToSync).to.be.an('array').that.has.lengthOf(2); + expect(r2b2.mappedParams).to.have.property(bid1Id); + expect(r2b2.mappedParams[bid1Id]).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1, pid: 'example.com/generic/300x250/1'}); + expect(r2b2.mappedParams).to.have.property(bid2Id); + expect(r2b2.mappedParams[bid2Id]).to.deep.equal({d: 'example.com', g: 'generic', p: '300x600', m: 0, pid: 'example.com/generic/300x600/0'}); + }); + + it('should pass gdpr properties', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {user, regs} = data; + expect(user).to.be.an('object').that.has.property('ext'); + expect(regs).to.be.an('object').that.has.property('ext'); + expect(user.ext.consent).to.equal('consent-string'); + expect(regs.ext.gdpr).to.equal(1); + }); + + it('should pass us privacy properties', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {regs} = data; + expect(regs).to.be.an('object').that.has.property('ext'); + expect(regs.ext.us_privacy).to.equal('1YYY'); + }); + + it('should pass supply chain', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {source} = data; + expect(source).to.be.an('object').that.has.property('ext'); + expect(source.ext.schain).to.deep.equal({ + complete: 1, + nodes: [ + {asi: 'example.com', hp: 1, sid: '00001'} + ], + ver: '1.0' + }) + }); + + it('should pass extended ids', function () { + let eidsArray = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID', + }, + id: 'TTD_ID_FROM_USER_ID_MODULE', + }, + ], + }, + { + source: 'pubcid.org', + uids: [ + { + atype: 1, + id: 'pubCommonId_FROM_USER_ID_MODULE', + }, + ], + }, + ]; + bids[0].userIdAsEids = eidsArray; + let requests = spec.buildRequests(bids, bidderRequest); + let request = requests[0]; + let eids = request.data.user.ext.eids; + + expect(eids).to.deep.equal(eidsArray); + }); + }); + + describe('interpretResponse', function () { + it('should respond with empty response when there are no bids', function () { + let result = spec.interpretResponse({ body: {} }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [ {} ] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [ { bids: [] } ] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + }); + + it('should map params correctly', function () { + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.requestId).to.equal(impId); + expect(bid.cpm).to.equal(price); + expect(bid.ad).to.equal(ad); + expect(bid.currency).to.equal('USD'); + expect(bid.mediaType).to.equal('banner'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.creativeId).to.equal(creativeId); + }); + + it('should set up renderer on bid', function () { + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.renderer).to.be.an('object'); + expect(bid.renderer).to.have.property('render').that.is.a('function'); + expect(bid.renderer).to.have.property('url').that.is.a('string'); + }); + + it('should map ext params correctly', function() { + let dgpm = {something: 'something'}; + r2b2.mappedParams = {}; + r2b2.mappedParams[impId] = dgpm; + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.ext).to.be.an('object'); + let { ext } = bid; + expect(ext.dgpm).to.deep.equal(dgpm); + expect(ext.cid).to.equal(cid); + expect(ext.cdid).to.equal(cdid); + expect(ext.adUnit).to.equal(unitCode); + expect(ext.mediaType).to.deep.equal({ + type: 'banner', + settings: { + chd: null, + width: 300, + height: 250, + ad: { + type: 'content', + data: ad + } + } + }); + }); + + it('should handle multiple bids', function() { + const impId2 = '123456'; + const price2 = 12; + const ad2 = 'gaeouho'; + const w2 = 300; + const h2 = 600; + let b = serverResponse.seatbid[0].bid[0]; + let b2 = Object.assign({}, b); + b2.impid = impId2; + b2.price = price2; + b2.adm = ad2; + b2.w = w2; + b2.h = h2; + serverResponse.seatbid[0].bid.push(b2); + requestForInterpretResponse.data.imp.push({id: impId2}); + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(2); + let firstBid = result[0]; + let secondBid = result[1]; + expect(firstBid.requestId).to.equal(impId); + expect(firstBid.ad).to.equal(ad); + expect(firstBid.cpm).to.equal(price); + expect(firstBid.width).to.equal(300); + expect(firstBid.height).to.equal(250); + expect(secondBid.requestId).to.equal(impId2); + expect(secondBid.ad).to.equal(ad2); + expect(secondBid.cpm).to.equal(price2); + expect(secondBid.width).to.equal(w2); + expect(secondBid.height).to.equal(h2); + }); + }); + + describe('getUserSyncs', function() { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + + it('should return an array with a sync for all bids', function() { + r2b2.placementsToSync = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(r2b2.placementsToSync); + const syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.be.an('array').that.has.lengthOf(1); + const sync = syncs[0]; + expect(sync).to.be.an('object'); + expect(sync.type).to.equal('iframe'); + expect(sync.url).to.include(`?p=${expectedEncodedIds}`); + }); + + it('should return the sync and include gdpr and usp parameters in the url', function() { + r2b2.placementsToSync = [id1Object, id2Object]; + const syncs = spec.getUserSyncs(syncOptions, {}, gdprConsent, usPrivacyString); + const sync = syncs[0]; + expect(sync).to.be.an('object'); + expect(sync.url).to.include(`&gdpr=1`); + expect(sync.url).to.include(`&gdpr_consent=${gdprConsent.consentString}`); + expect(sync.url).to.include(`&us_privacy=${usPrivacyString}`); + }); + }); + + describe('events', function() { + beforeEach(function() { + time = sinon.useFakeTimers(fakeTime); + sinon.stub(utils, 'triggerPixel'); + r2b2.mappedParams = {}; + r2b2.mappedParams[bidId1] = id1Object; + r2b2.mappedParams[bidId2] = id2Object; + bidStub = { + adserverTargeting: { hb_bidder: bidder, hb_pb: '10.00', hb_size: '300x300' }, + cpm: 10, + currency: 'USD', + ext: { + dgpm: { d: 'r2b2.cz', g: 'generic', m: 1, p: '300x300', pid: 'r2b2.cz/generic/300x300/1' } + }, + params: [ { pid: 'r2b2.cz/generic/300x300/1' } ], + }; + }); + afterEach(function() { + utils.triggerPixel.restore(); + time.restore(); + }); + + describe('onBidWon', function () { + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel with passed url', function () { + bidStub.ext.events = { + onBidWon: bidWonUrl, + onSetTargeting: setTargetingUrl + }; + const response = spec.onBidWon(bidStub); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.calledWithMatch(bidWonUrl)).to.equal(true); + }); + it('should not trigger a pixel if url is not available', function () { + bidStub.ext.events = null; + spec.onBidWon(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + bidStub.ext.events = { + onBidWon: '', + onSetTargeting: '', + }; + spec.onBidWon(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); + + describe('onSetTargeting', function () { + it('exists and is a function', () => { + expect(spec.onSetTargeting).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel with passed url', function () { + bidStub.ext.events = { + onBidWon: bidWonUrl, + onSetTargeting: setTargetingUrl + }; + const response = spec.onSetTargeting(bidStub); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.calledWithMatch(setTargetingUrl)).to.equal(true); + }); + it('should not trigger a pixel if url is not available', function () { + bidStub.ext.events = null; + spec.onSetTargeting(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + bidStub.ext.events = { + onBidWon: '', + onSetTargeting: '', + }; + spec.onSetTargeting(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); + + describe('onTimeout', function () { + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel', function () { + const bids = [bid1, bid2]; + const response = spec.onTimeout(bids); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.callCount).to.equal(1); + }); + it('should not trigger a pixel if no bids available', function () { + const bids = []; + spec.onTimeout(bids); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + it('should trigger a pixel with correct ids and a cache buster', function () { + const bids = [bid1, bidForeign1, bidForeign2, bid2, bidWithBadSetup]; + const expectedIds = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(expectedIds); + spec.onTimeout(bids); + expect(utils.triggerPixel.callCount).to.equal(1); + const triggeredUrl = utils.triggerPixel.args[0][0]; + expect(triggeredUrl).to.include(`p=${expectedEncodedIds}`); + expect(triggeredUrl.match(cacheBusterRegex)).to.exist; + }); + }); + + describe('onBidderError', function () { + it('exists and is a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel', function () { + const bidderRequest = { bids: [bid1, bid2] }; + const response = spec.onBidderError({ bidderRequest }); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.callCount).to.equal(1); + }); + it('should not trigger a pixel if no bids available', function () { + const bidderRequest = { bids: [] }; + spec.onBidderError({ bidderRequest }); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + it('should call triggerEvent with correct ids and a cache buster', function () { + const bids = [bid1, bid2, bidWithBadSetup] + const bidderRequest = { bids }; + const expectedIds = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(expectedIds); + spec.onBidderError({ bidderRequest }); + expect(utils.triggerPixel.callCount).to.equal(1); + const triggeredUrl = utils.triggerPixel.args[0][0]; + expect(triggeredUrl).to.include(`p=${expectedEncodedIds}`); + expect(triggeredUrl.match(cacheBusterRegex)).to.exist; + }); + }); + }); +}); diff --git a/test/spec/modules/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index bfa72a2510e..719e15ad695 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -1,6 +1,7 @@ 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&'; @@ -192,5 +193,56 @@ describe('rasBidAdapter', function () { const resp = spec.interpretResponse({ body: res }, {}); expect(resp).to.deep.equal([]); }); + + it('should generate auctionConfig when fledge is enabled', function () { + let bidRequest = { + method: 'GET', + url: 'https://example.com', + bidIds: [{ + slot: 'top', + bidId: '123', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: true + }, + { + slot: 'top', + bidId: '456', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: false + }] + }; + + let auctionConfigs = [{ + 'bidId': '123', + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': 'https://csr.onet.pl/testnetwork/v1/protected-audience-api/decision-logic.js', + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + 'sizes': ['300x250'], + 'gctx': '1234567890' + } + } + }]; + const resp = spec.interpretResponse({body: {gctx: '1234567890'}}, bidRequest); + expect(resp).to.deep.equal({bids: [], fledgeAuctionConfigs: auctionConfigs}); + }); }); }); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 7778e9cbf80..f0d019913e8 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -239,6 +239,7 @@ describe('RelaidoAdapter', function () { const request = data.bids[0]; expect(bidRequests.method).to.equal('POST'); expect(bidRequests.url).to.equal('https://api.relaido.jp/bid/v1/sprebid'); + expect(data.canonical_url).to.equal('https://publisher.com/home'); expect(data.canonical_url_hash).to.equal('e6092f44a0044903ae3764126eedd6187c1d9f04'); expect(data.ref).to.equal(bidderRequest.refererInfo.page); expect(data.timeout_ms).to.equal(bidderRequest.timeout); @@ -317,6 +318,23 @@ describe('RelaidoAdapter', function () { expect(data.bids).to.have.lengthOf(1); expect(data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); }); + + it('should get userIdAsEids', function () { + const userIdAsEids = [ + { + source: 'hogehoge.com', + uids: { + atype: 1, + id: 'hugahuga' + } + } + ] + bidRequest.userIdAsEids = userIdAsEids + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + const data = JSON.parse(bidRequests.data); + expect(data.bids[0].userIdAsEids).to.have.lengthOf(1); + expect(data.bids[0].userIdAsEids[0].source).to.equal('hogehoge.com'); + }); }); describe('spec.interpretResponse', function () { @@ -325,6 +343,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponse.body.ads[0].placementId); expect(response.width).to.equal(serverRequest.data.bids[0].width); expect(response.height).to.equal(serverRequest.data.bids[0].height); expect(response.cpm).to.equal(serverResponse.body.ads[0].price); @@ -343,6 +362,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponse.body.ads[0].placementId); expect(response.width).to.equal(serverRequest.data.bids[0].width); expect(response.height).to.equal(serverRequest.data.bids[0].height); expect(response.cpm).to.equal(serverResponse.body.ads[0].price); @@ -360,6 +380,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponseBanner.body.ads[0].placementId); expect(response.cpm).to.equal(serverResponseBanner.body.ads[0].price); expect(response.currency).to.equal(serverResponseBanner.body.ads[0].currency); expect(response.creativeId).to.equal(serverResponseBanner.body.ads[0].creativeId); diff --git a/test/spec/modules/relevantdigitalBidAdapter_spec.js b/test/spec/modules/relevantdigitalBidAdapter_spec.js index b2a5495b3cb..0e21453c8ba 100644 --- a/test/spec/modules/relevantdigitalBidAdapter_spec.js +++ b/test/spec/modules/relevantdigitalBidAdapter_spec.js @@ -1,5 +1,10 @@ import {spec, resetBidderConfigs} from 'modules/relevantdigitalBidAdapter.js'; import { parseUrl, deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; +import CONSTANTS from 'src/constants.json'; + +import adapterManager, { +} from 'src/adapterManager.js'; const expect = require('chai').expect; @@ -9,14 +14,29 @@ const ACCOUNT_ID = 'example_account_id'; const TEST_DOMAIN = 'example.com'; const TEST_PAGE = `https://${TEST_DOMAIN}/page.html`; -const BID_REQUEST = -{ - 'bidder': 'relevantdigital', +const CONFIG = { + enabled: true, + endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT, + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer', + bidders: ['relevantdigital'], + accountId: 'abc' +}; + +const ADUNIT_CODE = '/19968336/header-bid-tag-0'; + +const BID_PARAMS = { 'params': { 'placementId': PLACEMENT_ID, 'accountId': ACCOUNT_ID, - 'pbsHost': PBS_HOST, - }, + 'pbsHost': PBS_HOST + } +}; + +const BID_REQUEST = { + 'bidder': 'relevantdigital', + ...BID_PARAMS, 'ortb2Imp': { 'ext': { 'tid': 'e13391ea-00f3-495d-99a6-d937990d73a9' @@ -32,7 +52,7 @@ const BID_REQUEST = ] } }, - 'adUnitCode': '/19968336/header-bid-tag-0', + 'adUnitCode': ADUNIT_CODE, 'transactionId': 'e13391ea-00f3-495d-99a6-d937990d73a9', 'sizes': [ [ @@ -292,4 +312,64 @@ describe('Relevant Digital Bid Adaper', function () { expect(allSyncs).to.deep.equal(expectedResult) }); }); + describe('transformBidParams', function () { + beforeEach(() => { + config.setConfig({ + s2sConfig: CONFIG, + }); + }); + afterEach(() => { + config.resetConfig(); + }); + + const adUnit = (params) => ({ + code: ADUNIT_CODE, + bids: [ + { + bidder: 'relevantdigital', + adUnitCode: ADUNIT_CODE, + params, + } + ] + }); + + const request = (params) => adapterManager.makeBidRequests([adUnit(params)], 123, 'auction-id', 123, [], {})[0]; + + it('transforms adunit bid params and config params correctly', function () { + config.setConfig({ + relevantdigital: { + pbsHost: PBS_HOST, + accountId: ACCOUNT_ID, + }, + }); + const adUnitParams = { placementId: PLACEMENT_ID }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: `https://${BID_PARAMS.params.pbsHost}`, 'pbsBufferMs': 250 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('does not transform bid params if placementId is missing', function () { + const adUnitParams = { ...BID_PARAMS.params, placementId: null }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + it('does not transform bid params s2s config is missing', function () { + config.resetConfig(); + const adUnitParams = BID_PARAMS.params; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + }) }); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index ea45ff7e0b0..20c60ca328a 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -4,6 +4,8 @@ import { spec } from 'modules/richaudienceBidAdapter.js'; import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import sinon from 'sinon'; describe('Richaudience adapter tests', function () { var DEFAULT_PARAMS_NEW_SIZES = [{ @@ -64,6 +66,30 @@ describe('Richaudience adapter tests', function () { user: {} }]; + var DEFAULT_PARAMS_VIDEO_TIMEOUT = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'richaudience', + params: [{ + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site' + }], + timeout: 3000, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }] + var DEFAULT_PARAMS_VIDEO_IN = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', @@ -879,6 +905,24 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('gpid').and.to.equal('/19968336/header-bid-tag-1#example-2'); }) + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('onTimeout exist as a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should send timeout', function () { + spec.onTimeout(DEFAULT_PARAMS_VIDEO_TIMEOUT); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.firstCall.args[0]).to.equal('https://s.richaudience.com/err/?ec=6&ev=3000&pla=ADb1f40rmi&int=PREBID&pltfm=&node=&dm=localhost:9876'); + }); + }); + describe('userSync', function () { it('Verifies user syncs iframe include', function () { config.setConfig({ diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index eed8d74f271..28bd123cb5d 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -22,6 +22,12 @@ describe('riseAdapter', function () { }); }); + describe('bid adapter', function () { + it('should have aliases', function () { + expect(spec.aliases).to.be.an('array').that.is.not.empty; + }); + }); + describe('isBidRequestValid', function () { const bid = { 'bidder': spec.code, @@ -53,7 +59,7 @@ describe('riseAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'org': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001', }, 'bidId': '299ffc8cca0b87', 'loop': 1, @@ -195,6 +201,16 @@ describe('riseAdapter', function () { expect(request.data.bids[1].mediaType).to.equal(BANNER) }); + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + it('should respect syncEnabled option', function() { config.setConfig({ userSync: { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index d04b8075134..f0e33ce940e 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -696,6 +696,16 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); + it('should correctly send cdep signal when requested', () => { + var badposRequest = utils.deepClone(bidderRequest); + badposRequest.bids[0].ortb2 = {device: {ext: {cdep: 3}}}; + + let [request] = spec.buildRequests(badposRequest.bids, badposRequest); + let data = parseQuery(request.data); + + expect(data['o_cdep']).to.equal('3'); + }); + it('ad engine query params should be ordered correctly', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -1577,6 +1587,22 @@ describe('the rubicon adapter', function () { expect(data['eid_catchall']).to.equal('11111^2'); }); + + it('should send rubiconproject special case', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js + clonedBid.userIdAsEids = [{ + source: 'rubiconproject.com', + uids: [{ + id: 'some-cool-id', + atype: 3 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_rubiconproject.com']).to.equal('some-cool-id'); + }); }); describe('Config user.id support', function () { @@ -1792,6 +1818,126 @@ describe('the rubicon adapter', function () { expect(data['tg_i.dfp_ad_unit_code']).to.equal('/a/b/c'); }); }); + + describe('client hints', function () { + let standardSuaObject; + beforeEach(function () { + standardSuaObject = { + source: 2, + platform: { + brand: 'macOS', + version: [ + '12', + '6', + '0' + ] + }, + browsers: [ + { + brand: 'Not.A/Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + brand: 'Chromium', + version: [ + '114', + '0', + '5735', + '198' + ] + }, + { + brand: 'Google Chrome', + version: [ + '114', + '0', + '5735', + '198' + ] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + }); + it('should send m_ch_* params if ortb2.device.sua object is there', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // How should fastlane query be constructed with default SUA + let expectedValues = { + m_ch_arch: 'x86', + m_ch_bitness: '64', + m_ch_ua: `"Not.A/Brand"|v="8","Chromium"|v="114","Google Chrome"|v="114"`, + m_ch_full_ver: `"Not.A/Brand"|v="8.0.0.0","Chromium"|v="114.0.5735.198","Google Chrome"|v="114.0.5735.198"`, + m_ch_mobile: '?0', + m_ch_platform: 'macOS', + m_ch_platform_ver: '12.6.0' + } + + // Build Fastlane call + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // Loop through expected values and if they do not match push an error + const errors = Object.entries(expectedValues).reduce((accum, [key, val]) => { + if (data[key] !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) + return accum; + }, []); + + // should be no errors + expect(errors).to.deep.equal([]); + }); + it('should not send invalid values for m_ch_*', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + + // Alter input SUA object + // send model + standardSuaObject.model = 'Suface Duo'; + // send mobile = 1 + standardSuaObject.mobile = 1; + + // make browsers not an array + standardSuaObject.browsers = 'My Browser'; + + // make platform not have version + delete standardSuaObject.platform.version; + + // delete architecture + delete standardSuaObject.architecture; + + // add SUA to bid + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // Build Fastlane request + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // should show new names + expect(data.m_ch_model).to.equal('Suface Duo'); + expect(data.m_ch_mobile).to.equal('?1'); + + // should still send platform + expect(data.m_ch_platform).to.equal('macOS'); + + // platform version not sent + expect(data).to.not.haveOwnProperty('m_ch_platform_ver'); + + // both ua and full_ver not sent because browsers not array + expect(data).to.not.haveOwnProperty('m_ch_ua'); + expect(data).to.not.haveOwnProperty('m_ch_full_ver'); + + // arch not sent + expect(data).to.not.haveOwnProperty('m_ch_arch'); + }); + }); }); if (FEATURES.VIDEO) { @@ -2235,16 +2381,6 @@ describe('the rubicon adapter', function () { bidderRequest = createVideoBidderRequest(); delete bidderRequest.bids[0].mediaTypes.video.linearity; expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // change api to an string, no good - bidderRequest = createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.api = 'string'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // delete api, no good - bidderRequest = createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.api; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); }); it('bid request is valid when video context is outstream', function () { @@ -2573,6 +2709,15 @@ describe('the rubicon adapter', function () { const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); expect(slotParams.kw).to.equal('a,b,c'); }); + + it('should pass along o_ae param when fledge is enabled', () => { + const localBidRequest = Object.assign({}, bidderRequest.bids[0]); + localBidRequest.ortb2Imp.ext.ae = true; + + const slotParams = spec.createSlotParams(localBidRequest, bidderRequest); + + expect(slotParams['o_ae']).to.equal(1) + }); }); describe('classifiedAsVideo', function () { @@ -2636,6 +2781,18 @@ describe('the rubicon adapter', function () { expect(request.data.imp).to.have.nested.property('[0].native'); }); + it('should not break if position is set and no video MT', function () { + const bidReq = addNativeToBidRequest(bidderRequest); + delete bidReq.bids[0].mediaTypes.banner; + bidReq.bids[0].params = { + position: 'atf' + } + let [request] = spec.buildRequests(bidReq.bids, bidReq); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(request.data.imp).to.have.nested.property('[0].native'); + }); + describe('that contains also a banner mediaType', function () { it('should send the banner to fastlane BUT NOT the native bid because missing params.video', function() { const bidReq = addNativeToBidRequest(bidderRequest); @@ -3281,6 +3438,43 @@ describe('the rubicon adapter', function () { expect(bids).to.be.lengthOf(0); }); + it('Should support recieving an auctionConfig and pass it along to Prebid', function () { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [{ + 'status': 'ok', + 'cpm': 0, + 'size_id': 15 + }], + 'component_auction_config': [{ + 'random': 'value', + 'bidId': '5432' + }, + { + 'random': 'string', + 'bidId': '6789' + }] + }; + + let {bids, fledgeAuctionConfigs} = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + + expect(bids).to.be.lengthOf(1); + expect(fledgeAuctionConfigs[0].bidId).to.equal('5432'); + expect(fledgeAuctionConfigs[0].config.random).to.equal('value'); + expect(fledgeAuctionConfigs[1].bidId).to.equal('6789'); + }); + it('should handle an error', function () { let response = { 'status': 'ok', diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index fb666e89f73..516c5ec933a 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -2,10 +2,24 @@ import { expect } from 'chai'; import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js'; import * as utils from 'src/utils.js'; import { config } from '../../../src/config.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; const PUBLISHER_ID = '0000-0000-01'; const ADUNIT_ID = '000000'; +const adUnitCode = '/19968336/header-bid-tag-0' + +// create a default adunit +const slot = document.createElement('div'); +slot.id = adUnitCode; +slot.style.width = '300px' +slot.style.height = '250px' +slot.style.position = 'absolute' +slot.style.top = '10px' +slot.style.left = '20px' + +document.body.appendChild(slot); + function getSlotConfigs(mediaTypes, params) { return { params: params, @@ -25,7 +39,7 @@ function getSlotConfigs(mediaTypes, params) { tid: 'd704d006-0d6e-4a09-ad6c-179e7e758096', } }, - adUnitCode: 'adunit-code', + adUnitCode: adUnitCode, }; } @@ -46,6 +60,13 @@ const createBannerSlotConfig = (placement, mediatypes) => { }; describe('Seedtag Adapter', function () { + beforeEach(function () { + mockGpt.reset(); + }); + + afterEach(function () { + mockGpt.enable(); + }); describe('isBidRequestValid method', function () { describe('returns true', function () { describe('when banner slot config has all mandatory params', () => { @@ -277,7 +298,7 @@ describe('Seedtag Adapter', function () { expect(data.auctionStart).to.be.greaterThanOrEqual(now); expect(data.ttfb).to.be.greaterThanOrEqual(0); - expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code'); + expect(data.bidRequests[0].adUnitCode).to.equal(adUnitCode); }); describe('GDPR params', function () { @@ -374,6 +395,35 @@ describe('Seedtag Adapter', function () { expect(videoBid.sizes[1][1]).to.equal(600); expect(videoBid.requestCount).to.equal(1); }); + + it('should have geom parameters if slot is available', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + const bidRequests = data.bidRequests; + const bannerBid = bidRequests[0]; + + // on some CI, the DOM is not initialized, so we need to check if the slot is available + const slot = document.getElementById(adUnitCode) + if (slot) { + expect(bannerBid).to.have.property('geom') + + const params = [['width', 300], ['height', 250], ['top', 10], ['left', 20], ['scrollY', 0]] + params.forEach(([param, value]) => { + expect(bannerBid.geom).to.have.property(param) + expect(bannerBid.geom[param]).to.be.a('number') + expect(bannerBid.geom[param]).to.be.equal(value) + }) + + expect(bannerBid.geom).to.have.property('viewport') + const viewportParams = ['width', 'height'] + viewportParams.forEach(param => { + expect(bannerBid.geom.viewport).to.have.property(param) + expect(bannerBid.geom.viewport[param]).to.be.a('number') + }) + } else { + expect(bannerBid).to.not.have.property('geom') + } + }) }); describe('COPPA param', function () { diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 68bf14ae9c1..6a63ae681e7 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -409,12 +409,12 @@ describe('sharethrough adapter spec', function () { it('should properly attach GPP information to the request when applicable', () => { bidderRequest.gppConsent = { gppString: 'some-gpp-string', - applicableSections: [3, 5] + applicableSections: [3, 5], }; const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; - expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString) - expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections) + expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); }); it('should populate request accordingly when gpp explicitly does not apply', function () { @@ -588,6 +588,43 @@ describe('sharethrough adapter spec', function () { }); }); + describe('cookie deprecation', () => { + it('should not add cdep if we do not get it in an impression request', () => { + const builtRequests = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: { + propThatIsNotCdep: 'value-we-dont-care-about', + }, + }, + }, + }); + const noCdep = builtRequests.every((builtRequest) => { + const ourCdepValue = builtRequest.data.device?.ext?.cdep; + return ourCdepValue === undefined; + }); + expect(noCdep).to.be.true; + }); + + it('should add cdep if we DO get it in an impression request', () => { + const builtRequests = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: { + cdep: 'cdep-value', + }, + }, + }, + }); + const cdepPresent = builtRequests.every((builtRequest) => { + return builtRequest.data.device.ext.cdep === 'cdep-value'; + }); + expect(cdepPresent).to.be.true; + }); + }); + describe('first party data', () => { const firstPartyData = { site: { @@ -618,7 +655,7 @@ describe('sharethrough adapter spec', function () { badv: ['domain1.com', 'domain2.com'], regs: { gpp: 'gpp_string', - gpp_sid: [7] + gpp_sid: [7], }, }; @@ -870,25 +907,6 @@ describe('sharethrough adapter spec', function () { const syncArray = spec.getUserSyncs({ pixelEnabled: false }, serverResponses); expect(syncArray).to.be.an('array').that.is.empty; }); - - it('returns GDPR Consent Params in UserSync url', function () { - const syncArray = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, { gdprApplies: true, - consentString: 'consent' }); - expect(syncArray).to.deep.equal([ - { type: 'image', url: 'cookieUrl1&gdpr=1&gdpr_consent=consent' }, - { type: 'image', url: 'cookieUrl2&gdpr=1&gdpr_consent=consent' }, - { type: 'image', url: 'cookieUrl3&gdpr=1&gdpr_consent=consent' }, - ]); - }); - - it('returns GPP Consent Params in UserSync url', function () { - const syncArray = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, {}, {gppString: 'gpp-string', applicableSections: [1, 2]}); - expect(syncArray).to.deep.equal([ - { type: 'image', url: 'cookieUrl1&gdpr=0&gdpr_consent=&gpp=gpp-string&gpp_sid=1%2C2' }, - { type: 'image', url: 'cookieUrl2&gdpr=0&gdpr_consent=&gpp=gpp-string&gpp_sid=1%2C2' }, - { type: 'image', url: 'cookieUrl3&gdpr=0&gdpr_consent=&gpp=gpp-string&gpp_sid=1%2C2' }, - ]); - }); }); }); }); diff --git a/test/spec/modules/shinezRtbBidAdapter_spec.js b/test/spec/modules/shinezRtbBidAdapter_spec.js new file mode 100644 index 00000000000..3965cd69c5f --- /dev/null +++ b/test/spec/modules/shinezRtbBidAdapter_spec.js @@ -0,0 +1,639 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/shinezRtbBidAdapter'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; +import {deepAccess} from 'src/utils.js'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId', 'digitrustid']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppConsent': { + 'gppString': 'gpp_string', + 'applicableSections': [7] + }, + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['sweetgum.io'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('ShinezRtbBidAdapter', function () { + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + shinezRtb: { + storageAllowed: true + } + }; + sandbox = sinon.sandbox.create(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + prebidVersion: version, + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + }, + gpid: '0123456789' + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.sweetgum.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.sweetgum.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }) + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['sweetgum.io'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['sweetgum.io'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['sweetgum.io'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'digitrustid': + return {data: {id}}; + case 'lipb': + return {lipbid: id}; + case 'parrableId': + return {eid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + shinezRtb: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + shinezRtb: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem('myKey', 2020); + const {value, created} = getStorageItem('myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem('myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 9daa6a87826..58b4cd8c0d0 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -786,7 +786,7 @@ describe('Smart bid adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); - expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(null); + expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); }); @@ -833,6 +833,73 @@ describe('Smart bid adapter tests', function () { expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(6); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(3); }); + + it('should pass additional parameters', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + api: [1, 2, 3], + maxbitrate: 50, + minbitrate: 20, + maxduration: 30, + minduration: 5, + placement: 3, + playbackmethod: [2, 4], + playerSize: [[640, 480]], + plcmt: 1, + skip: 0 + } + }, + params: { + siteId: '123' + } + }]); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).to.have.property('iabframeworks').and.to.equal('1,2,3'); + expect(requestContent.videoData).not.to.have.property('skip'); + expect(requestContent.videoData).to.have.property('vbrmax').and.to.equal(50); + expect(requestContent.videoData).to.have.property('vbrmin').and.to.equal(20); + expect(requestContent.videoData).to.have.property('vdmax').and.to.equal(30); + expect(requestContent.videoData).to.have.property('vdmin').and.to.equal(5); + expect(requestContent.videoData).to.have.property('vplcmt').and.to.equal(1); + expect(requestContent.videoData).to.have.property('vpmt').and.to.have.lengthOf(2); + expect(requestContent.videoData.vpmt[0]).to.equal(2); + expect(requestContent.videoData.vpmt[1]).to.equal(4); + expect(requestContent.videoData).to.have.property('vpt').and.to.equal(3); + }); + + it('should not pass not valuable parameters', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + maxbitrate: 20, + minbitrate: null, + maxduration: 0, + playbackmethod: [], + playerSize: [[640, 480]], + plcmt: 1 + } + }, + params: { + siteId: '123' + } + }]); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).not.to.have.property('iabframeworks'); + expect(requestContent.videoData).to.have.property('vbrmax').and.to.equal(20); + expect(requestContent.videoData).not.to.have.property('vbrmin'); + expect(requestContent.videoData).not.to.have.property('vdmax'); + expect(requestContent.videoData).not.to.have.property('vdmin'); + expect(requestContent.videoData).to.have.property('vplcmt').and.to.equal(1); + expect(requestContent.videoData).not.to.have.property('vpmt'); + expect(requestContent.videoData).not.to.have.property('vpt'); + }); }); }); @@ -1029,7 +1096,7 @@ describe('Smart bid adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); - expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(null); + expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); }); @@ -1393,4 +1460,41 @@ describe('Smart bid adapter tests', function () { expect(requestContent).to.have.property('gpid').and.to.equal(gpid); }); }); + + describe('#getValuableProperty method', function () { + it('should return an object when calling with a number value', () => { + const obj = spec.getValuableProperty('prop', 3); + expect(obj).to.deep.equal({ prop: 3 }); + }); + + it('should return an empty object when calling with a string value', () => { + const obj = spec.getValuableProperty('prop', 'str'); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a number property', () => { + const obj = spec.getValuableProperty(7, 'str'); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a null value', () => { + const obj = spec.getValuableProperty('prop', null); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with an object value', () => { + const obj = spec.getValuableProperty('prop', {}); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a 0 value', () => { + const obj = spec.getValuableProperty('prop', 0); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling without the value argument', () => { + const obj = spec.getValuableProperty('prop'); + expect(obj).to.deep.equal({}); + }); + }); }); diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index 350cad33704..458ccc37759 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -52,7 +52,11 @@ describe('SmartyadsAdapter', function () { expect(serverRequest.method).to.equal('POST'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'); + expect(serverRequest.url).to.be.oneOf([ + 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' + ]); }); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; @@ -280,7 +284,21 @@ describe('SmartyadsAdapter', function () { }); it('should send a valid bid won notice', function () { - spec.onBidWon(bidResponse); + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'winTest': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidWon(bid); expect(server.requests.length).to.equal(1); }); }); @@ -291,7 +309,21 @@ describe('SmartyadsAdapter', function () { }); it('should send a valid bid timeout notice', function () { - spec.onTimeout({}); + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidTimeout': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onTimeout(bid); expect(server.requests.length).to.equal(1); }); }); @@ -302,7 +334,21 @@ describe('SmartyadsAdapter', function () { }); it('should send a valid bidder error notice', function () { - spec.onBidderError({}); + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidderError': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidderError(bid); expect(server.requests.length).to.equal(1); }); }); diff --git a/test/spec/modules/snigelBidAdapter_spec.js b/test/spec/modules/snigelBidAdapter_spec.js index 7fe2387ca6c..3ba84228872 100644 --- a/test/spec/modules/snigelBidAdapter_spec.js +++ b/test/spec/modules/snigelBidAdapter_spec.js @@ -23,6 +23,7 @@ const BASE_BIDDER_REQUEST = { auctionId: 'test', bidderRequestId: 'test', refererInfo: { + page: 'https://localhost', canonicalUrl: 'https://localhost', }, }; diff --git a/test/spec/modules/sonobiAnalyticsAdapter_spec.js b/test/spec/modules/sonobiAnalyticsAdapter_spec.js index 76ff88836d4..ed8ccd22eea 100644 --- a/test/spec/modules/sonobiAnalyticsAdapter_spec.js +++ b/test/spec/modules/sonobiAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import sonobiAnalytics from 'modules/sonobiAnalyticsAdapter.js'; +import sonobiAnalytics, {DEFAULT_EVENT_URL} from 'modules/sonobiAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; let events = require('src/events'); @@ -76,8 +76,8 @@ describe('Sonobi Prebid Analytic', function () { events.emit(constants.EVENTS.AUCTION_END, {auctionId: '13', bidsReceived: [bid]}); clock.tick(5000); - expect(server.requests).to.have.length(1); - expect(JSON.parse(server.requests[0].requestBody)).to.have.length(3) + const req = server.requests.find(req => req.url.indexOf(DEFAULT_EVENT_URL) !== -1); + expect(JSON.parse(req.requestBody)).to.have.length(3) done(); }); }); diff --git a/test/spec/modules/sonobiBidAdapter_spec.js b/test/spec/modules/sonobiBidAdapter_spec.js index de8d0a5bda7..83db7c0a812 100644 --- a/test/spec/modules/sonobiBidAdapter_spec.js +++ b/test/spec/modules/sonobiBidAdapter_spec.js @@ -1,9 +1,9 @@ -import { expect } from 'chai' -import { spec, _getPlatform } from 'modules/sonobiBidAdapter.js' -import { newBidder } from 'src/adapters/bidderFactory.js' +import { expect } from 'chai'; +import { _getPlatform, spec } from 'modules/sonobiBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import { userSync } from '../../../src/userSync.js'; import { config } from 'src/config.js'; -import * as utils from '../../../src/utils.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; describe('SonobiBidAdapter', function () { const adapter = newBidder(spec) @@ -248,13 +248,13 @@ describe('SonobiBidAdapter', function () { let sandbox; beforeEach(function () { sinon.stub(userSync, 'canBidderRegisterSync'); - sinon.stub(utils, 'getGptSlotInfoForAdUnitCode') + sinon.stub(gptUtils, 'getGptSlotInfoForAdUnitCode') .onFirstCall().returns({ gptSlot: '/123123/gpt_publisher/adunit-code-3', divId: 'adunit-code-3-div-id' }); sandbox = sinon.createSandbox(); }); afterEach(function () { userSync.canBidderRegisterSync.restore(); - utils.getGptSlotInfoForAdUnitCode.restore(); + gptUtils.getGptSlotInfoForAdUnitCode.restore(); sandbox.restore(); }); let bidRequest = [{ @@ -295,7 +295,10 @@ describe('SonobiBidAdapter', function () { mediaTypes: { video: { playerSize: [640, 480], - context: 'outstream' + context: 'outstream', + playbackmethod: [1, 2, 3], + plcmt: 3, + placement: 2 } } }, @@ -339,7 +342,7 @@ describe('SonobiBidAdapter', function () { }]; let keyMakerData = { - '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,', + '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,pm=1:2:3,p=2,pl=3,', '30b31c1838de1d': '1a2b3c4d5e6f1a2b3c4e|300x250,300x600|f=0.42,gpid=/123123/gpt_publisher/adunit-code-3,c=d,', '/7780971/sparks_prebid_LB|30b31c1838de1e': '300x250,300x600|gpid=/7780971/sparks_prebid_LB,c=d,', }; @@ -356,7 +359,9 @@ describe('SonobiBidAdapter', function () { 'page': 'https://example.com', 'stack': ['https://example.com'] }, - uspConsent: 'someCCPAString' + uspConsent: 'someCCPAString', + ortb2: {} + }; it('should set fpd if there is any data in ortb2', function () { @@ -490,6 +495,14 @@ describe('SonobiBidAdapter', function () { expect(bidRequests.data.hfa).to.equal('hfakey') }) + it('should return a properly formatted request with expData and expKey', function () { + bidderRequests.ortb2.experianRtidData = 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='; + bidderRequests.ortb2.experianRtidKey = 'sovrn-encryption-key-1'; + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.data.expData).to.equal('IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='); + expect(bidRequests.data.expKey).to.equal('sovrn-encryption-key-1'); + }) + it('should return null if there is nothing to bid on', function () { const bidRequests = spec.buildRequests([{ params: {} }], bidderRequests) expect(bidRequests).to.equal(null); diff --git a/test/spec/modules/sovrnAnalyticsAdapter_spec.js b/test/spec/modules/sovrnAnalyticsAdapter_spec.js index 68552eb3d8a..973e90abd5a 100644 --- a/test/spec/modules/sovrnAnalyticsAdapter_spec.js +++ b/test/spec/modules/sovrnAnalyticsAdapter_spec.js @@ -12,7 +12,7 @@ let constants = require('src/constants.json'); /** * Emit analytics events - * @param {array} eventArr - array of objects to define the events that will fire + * @param {Array} eventArr - array of objects to define the events that will fire * @param {object} eventObj - key is eventType, value is event * @param {string} auctionId - the auction id to attached to the events */ diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 90913c6f130..f165a6da6d1 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -64,6 +64,29 @@ describe('sovrnBidAdapter', function() { expect(spec.isBidRequestValid(bidRequest)).to.equal(false) }) + + it('should return true when minduration is not passed', function() { + const width = 300 + const height = 250 + const mimes = ['video/mp4', 'application/javascript'] + const protocols = [2, 5] + const maxduration = 60 + const startdelay = 0 + const videoBidRequest = { + ...baseBidRequest, + mediaTypes: { + video: { + mimes, + protocols, + playerSize: [[width, height], [360, 240]], + maxduration, + startdelay + } + } + } + + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true) + }) }) describe('buildRequests', function () { @@ -295,6 +318,41 @@ describe('sovrnBidAdapter', function() { expect(data.regs.ext['us_privacy']).to.equal(bidderRequest.uspConsent) }) + it('should not set coppa when coppa is undefined', function () { + const bidderRequest = { + ...baseBidderRequest, + bidderCode: 'sovrn', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + bids: [baseBidRequest], + gdprConsent: { + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + gdprApplies: true + }, + } + const {regs} = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(regs.coppa).to.be.undefined + }) + + it('should set coppa to 1 when coppa is provided with value true', function () { + const bidderRequest = { + ...baseBidderRequest, + ortb2: { + regs: { + coppa: true + } + }, + bidderCode: 'sovrn', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + bids: [baseBidRequest] + } + const {regs} = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(regs.coppa).to.equal(1) + }) + it('should send gpp info in OpenRTB 2.6 location when gppConsent defined', function () { const bidderRequest = { ...baseBidderRequest, diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js new file mode 100644 index 00000000000..293f7da30a1 --- /dev/null +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -0,0 +1,467 @@ +import {expect} from 'chai'; +import { deepClone, mergeDeep } from 'src/utils'; +import {spec as adapter} from 'modules/sparteoBidAdapter'; + +const CURRENCY = 'EUR'; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; + +const VALID_BID_BANNER = { + bidder: 'sparteo', + bidId: '1a2b3c4d', + adUnitCode: 'id-1234', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + formats: ['corner'] + }, + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + } +}; + +const VALID_BID_VIDEO = { + bidder: 'sparteo', + bidId: '5e6f7g8h', + adUnitCode: 'id-5678', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + }, + mediaTypes: { + video: { + playerSize: [640, 360], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + api: [1, 2], + mimes: ['video/mp4'], + skip: 1, + startdelay: 0, + placement: 1, + linearity: 1, + minduration: 5, + maxduration: 30, + context: 'instream' + } + }, + ortb2Imp: { + ext: { + pbadslot: 'video' + } + } +}; + +const VALID_REQUEST_BANNER = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + }, + 'ext': { + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'formats': ['corner'] + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST_VIDEO = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video', + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + }, + 'ext': { + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'formats': ['corner'] + } + } + } + }, { + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video', + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const BIDDER_REQUEST = { + bids: [VALID_BID_BANNER, VALID_BID_VIDEO] +} + +const BIDDER_REQUEST_BANNER = { + bids: [VALID_BID_BANNER] +} + +const BIDDER_REQUEST_VIDEO = { + bids: [VALID_BID_VIDEO] +} + +describe('SparteoAdapter', function () { + describe('isBidRequestValid', function () { + describe('Check method return', function () { + it('should return true', function () { + expect(adapter.isBidRequestValid(VALID_BID_BANNER)).to.equal(true); + expect(adapter.isBidRequestValid(VALID_BID_VIDEO)).to.equal(true); + }); + + it('should return false because the networkId is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + delete wrongBid.params.networkId; + + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the banner size is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + + wrongBid.mediaTypes.banner.sizes = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.banner.sizes; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the video player size paramater is missing', function () { + let wrongBid = deepClone(VALID_BID_VIDEO); + + wrongBid.mediaTypes.video.playerSize = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.video.playerSize; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('Check method return', function () { + if (FEATURES.VIDEO) { + it('should return the right formatted requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST); + }); + } + + it('should return the right formatted banner requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_BANNER); + }); + + if (FEATURES.VIDEO) { + it('should return the right formatted video requests', function() { + const request = adapter.buildRequests([VALID_BID_VIDEO], BIDDER_REQUEST_VIDEO); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_VIDEO); + }); + } + + it('should return the right formatted request with endpoint test', function() { + let endpoint = 'https://bid-test.sparteo.com/auction'; + + let bids = mergeDeep(deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]), { + params: { + endpoint: endpoint + } + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST)); + + const request = adapter.buildRequests(bids, BIDDER_REQUEST); + requests.url = endpoint; + delete request.data.id; + + expect(requests).to.deep.equal(requests); + }); + }); + }); + + describe('interpretResponse', function() { + describe('Check method return', function () { + it('should return the right formatted response', function() { + let response = { + body: { + 'id': '63f4d300-6896-4bdc-8561-0932f73148b1', + 'cur': 'EUR', + 'seatbid': [ + { + 'seat': 'sparteo', + 'group': 0, + 'bid': [ + { + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87a', + 'impid': '1a2b3c4d', + 'price': 4.5, + 'ext': { + 'prebid': { + 'type': 'banner' + } + }, + 'adm': 'script', + 'crid': 'crid', + 'w': 1, + 'h': 1, + 'nurl': 'https://t.bidder.sparteo.com/img' + } + ] + } + ] + } + }; + + if (FEATURES.VIDEO) { + response.body.seatbid[0].bid.push({ + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87b', + 'impid': '5e6f7g8h', + 'price': 5, + 'ext': { + 'prebid': { + 'type': 'video', + 'cache': { + 'vastXml': { + 'url': 'https://pbs.tet.com/cache?uuid=1234' + } + } + } + }, + 'adm': 'tag', + 'crid': 'crid', + 'w': 640, + 'h': 480, + 'nurl': 'https://t.bidder.sparteo.com/img' + }); + } + + let formattedReponse = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
' + } + ]; + + if (FEATURES.VIDEO) { + formattedReponse.push({ + requestId: '5e6f7g8h', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + cpm: 5, + width: 640, + height: 480, + playerWidth: 640, + playerHeight: 360, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + nurl: 'https://t.bidder.sparteo.com/img', + vastUrl: 'https://pbs.tet.com/cache?uuid=1234', + vastXml: 'tag' + }); + } + + if (FEATURES.VIDEO) { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } else { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } + }); + }); + }); + + describe('onBidWon', function() { + describe('Check methods succeed', function () { + it('should not throw error', function() { + let bids = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
', + nurl: [ + 'win.domain.com' + ] + }, + { + requestId: '2570', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + id: 'id-5678', + cpm: 5, + width: 640, + height: 480, + creativeId: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastXml: 'vast xml', + nurl: [ + 'win.domain2.com' + ] + } + ]; + + bids.forEach(function(bid) { + expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); + }); + }); + }); + }); + + describe('getUserSyncs', function() { + describe('Check methods succeed', function () { + it('should return the sync url', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + }; + const gdprConsent = { + gdprApplies: 1, + consentString: 'tcfv2' + }; + const uspConsent = { + consentString: '1Y---' + }; + + const syncUrls = [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + '&gdpr=1&gdpr_consent=tcfv2&usp_consent=1Y---' + }]; + + expect(adapter.getUserSyncs(syncOptions, null, gdprConsent, uspConsent)).to.deep.equal(syncUrls); + }); + }); + }); +}); diff --git a/test/spec/modules/stvBidAdapter_spec.js b/test/spec/modules/stvBidAdapter_spec.js index 41f29cced34..3ef865ed2f1 100644 --- a/test/spec/modules/stvBidAdapter_spec.js +++ b/test/spec/modules/stvBidAdapter_spec.js @@ -71,6 +71,24 @@ describe('stvAdapter', function() { 'hp': 1 } ] + }, + 'userId': { + 'id5id': { + 'uid': '1234', + 'ext': { + 'linkType': 'abc' + } + }, + 'netId': '2345', + 'uid2': { + 'id': '3456', + }, + 'sharedid': { + 'id': '4567', + }, + 'idl_env': '5678', + 'criteoId': '6789', + 'utiq': '7890', } }, { @@ -84,7 +102,27 @@ describe('stvAdapter', function() { ], 'bidId': '30b31c1838de1e2', 'bidderRequestId': '22edbae2733bf62', - 'auctionId': '1d1a030790a476' + 'auctionId': '1d1a030790a476', + 'userId': { // with other utiq variant + 'id5id': { + 'uid': '1234', + 'ext': { + 'linkType': 'abc' + } + }, + 'netId': '2345', + 'uid2': { + 'id': '3456', + }, + 'sharedid': { + 'id': '4567', + }, + 'idl_env': '5678', + 'criteoId': '6789', + 'utiq': { + 'id': '7890' + }, + } }, { 'bidder': 'stv', 'params': { @@ -181,7 +219,7 @@ describe('stvAdapter', function() { expect(request1.method).to.equal('GET'); expect(request1.url).to.equal(ENDPOINT_URL); let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&schain=1.0,0!reseller.com,aaaaa,1,BidRequest4,,,&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&bcat=IAB2%2CIAB4&dvt=desktop&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&schain=1.0,0!reseller.com,aaaaa,1,BidRequest4,,&uids=id5%3A1234,id5_linktype%3Aabc,netid%3A2345,uid2%3A3456,sharedid%3A4567,liverampid%3A5678,criteoid%3A6789,utiq%3A7890&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&bcat=IAB2%2CIAB4&dvt=desktop&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); }); var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; @@ -189,7 +227,7 @@ describe('stvAdapter', function() { expect(request2.method).to.equal('GET'); expect(request2.url).to.equal(ENDPOINT_URL); let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=html&alternative=prebid_js&_ps=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&prebidDevMode=1&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&uids=id5%3A1234,id5_linktype%3Aabc,netid%3A2345,uid2%3A3456,sharedid%3A4567,liverampid%3A5678,criteoid%3A6789,utiq%3A7890&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&prebidDevMode=1&media_types%5Bbanner%5D=300x250'); }); // Without gdprConsent diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 7d31e291667..dd91c410d08 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config' import * as utils from '../../../src/utils' import {server} from '../../mocks/xhr' @@ -113,6 +113,50 @@ describe('Taboola Adapter', function () { }); }); + describe('onTimeout', function () { + it('onTimeout exist as a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should send timeout', function () { + const timeoutData = [{ + bidder: 'taboola', + bidId: 'da43860a-4644-442a-b5e0-93f268cf8d19', + params: [{ + publisherId: 'publisherId' + }], + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c' + }] + spec.onTimeout(timeoutData); + expect(server.requests[0].method).to.equal('POST'); + expect(server.requests[0].url).to.equal(EVENT_ENDPOINT + '/timeout'); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal(timeoutData); + }); + }); + + describe('onBidderError', function () { + it('onBidderError exist as a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should send bidder error', function () { + const error = { + status: 204, + statusText: 'No Content' + }; + const bidderRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId' + } + } + spec.onBidderError({error, bidderRequest}); + expect(server.requests[0].method).to.equal('POST'); + expect(server.requests[0].url).to.equal(EVENT_ENDPOINT + '/bidError'); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal(error, bidderRequest); + }); + }); + describe('buildRequests', function () { const defaultBidRequest = { ...createBidRequest(), @@ -129,10 +173,10 @@ describe('Taboola Adapter', function () { } it('should build display request', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); const expectedData = { - id: 'mock-uuid', 'imp': [{ - 'id': 1, + 'id': res.data.imp[0].id, 'banner': { format: [{ w: displayBidRequestParams.sizes[0][0], @@ -149,6 +193,8 @@ describe('Taboola Adapter', function () { 'bidfloorcur': 'USD', 'ext': {} }], + id: 'mock-uuid', + 'test': 0, 'site': { 'id': commonBidRequest.params.publisherId, 'name': commonBidRequest.params.publisherId, @@ -171,10 +217,8 @@ describe('Taboola Adapter', function () { 'ext': {} }; - const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); - - expect(res.url).to.equal(`${END_POINT_URL}/${commonBidRequest.params.publisherId}`); - expect(res.data).to.deep.equal(JSON.stringify(expectedData)); + expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); + expect(JSON.stringify(res.data)).to.deep.equal(JSON.stringify(expectedData)); }); it('should pass optional parameters in request', function () { @@ -189,9 +233,8 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(0.25); - expect(resData.imp[0].bidfloorcur).to.deep.equal('EUR'); + expect(res.data.imp[0].bidfloor).to.deep.equal(0.25); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('EUR'); }); it('should pass bid floor', function () { @@ -206,9 +249,8 @@ describe('Taboola Adapter', function () { } }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(2.7); - expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); + expect(res.data.imp[0].bidfloor).to.deep.equal(2.7); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('USD'); }); it('should pass bid floor even if it is a bid floor param', function () { @@ -228,9 +270,8 @@ describe('Taboola Adapter', function () { } }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(2.7); - expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); + expect(res.data.imp[0].bidfloor).to.deep.equal(2.7); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('USD'); }); it('should pass impression position', function () { @@ -244,8 +285,7 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].banner.pos).to.deep.equal(2); + expect(res.data.imp[0].banner.pos).to.deep.equal(2); }); it('should pass gpid if configured', function () { @@ -261,8 +301,23 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + expect(res.data.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + }); + + it('should pass new parameter to imp ext', function () { + const ortb2Imp = { + ext: { + example: 'example' + } + } + const bidRequest = { + ...defaultBidRequest, + ortb2Imp: ortb2Imp, + params: {...commonBidRequest.params} + }; + + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.example).to.deep.equal('example'); }); it('should pass bidder timeout', function () { @@ -271,8 +326,25 @@ describe('Taboola Adapter', function () { timeout: 500 } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.tmax).to.equal(500); + expect(res.data.tmax).to.equal(500); + }); + + it('should pass bidder tmax as int', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: '500' + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.tmax).to.equal(500); + }); + + it('should pass bidder timeout as null', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: null + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.tmax).to.equal(undefined); }); describe('first party data', function () { @@ -286,10 +358,9 @@ describe('Taboola Adapter', function () { } } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.bcat).to.deep.equal(bidderRequest.ortb2.bcat) - expect(resData.badv).to.deep.equal(bidderRequest.ortb2.badv) - expect(resData.wlang).to.deep.equal(bidderRequest.ortb2.wlang) + expect(res.data.bcat).to.deep.equal(bidderRequest.ortb2.bcat) + expect(res.data.badv).to.deep.equal(bidderRequest.ortb2.badv) + expect(res.data.wlang).to.deep.equal(bidderRequest.ortb2.wlang) }); it('should pass pageType if exists in ortb2', function () { @@ -304,8 +375,20 @@ describe('Taboola Adapter', function () { } } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + expect(res.data.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + }); + + it('should pass additional parameter in request', function () { + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + ext: { + example: 'example' + } + } + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.ext.example).to.deep.equal(bidderRequest.ortb2.ext.example); }); }); @@ -322,9 +405,8 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([defaultBidRequest], bidderRequest) - const resData = JSON.parse(res.data) - expect(resData.user.ext.consent).to.equal('consentString') - expect(resData.regs.ext.gdpr).to.equal(1) + expect(res.data.user.ext.consent).to.equal('consentString') + expect(res.data.regs.ext.gdpr).to.equal(1) }); it('should pass GPP consent if exist in ortb2', function () { @@ -336,9 +418,8 @@ describe('Taboola Adapter', function () { } const res = spec.buildRequests([defaultBidRequest], {...commonBidderRequest, ortb2}) - const resData = JSON.parse(res.data) - expect(resData.regs.ext.gpp).to.equal('testGpp') - expect(resData.regs.ext.gpp_sid).to.deep.equal([1, 2, 3]) + expect(res.data.regs.ext.gpp).to.equal('testGpp') + expect(res.data.regs.ext.gpp_sid).to.deep.equal([1, 2, 3]) }); it('should pass us privacy consent', function () { @@ -349,16 +430,14 @@ describe('Taboola Adapter', function () { uspConsent: 'consentString' } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.regs.ext.us_privacy).to.equal('consentString'); + expect(res.data.regs.ext.us_privacy).to.equal('consentString'); }); it('should pass coppa consent', function () { config.setConfig({coppa: true}) const res = spec.buildRequests([defaultBidRequest], commonBidderRequest) - const resData = JSON.parse(res.data); - expect(resData.regs.coppa).to.equal(1) + expect(res.data.regs.coppa).to.equal(1) config.resetConfig() }); @@ -375,8 +454,7 @@ describe('Taboola Adapter', function () { timeout: 500 } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(51525152); + expect(res.data.user.buyeruid).to.equal(51525152); }); it('should get user id from cookie if local storage isn`t defined', function () { @@ -390,9 +468,22 @@ describe('Taboola Adapter', function () { ...commonBidderRequest }; const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); + expect(res.data.user.buyeruid).to.equal('12121212'); + }); + + it('should get user id from tgid cookie if local storage isn`t defined', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.returns('d966c5be-c49f-4f73-8cd1-37b6b5790653-tuct9f7bf10'); + + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); - expect(resData.user.buyeruid).to.equal('12121212'); + expect(res.data.user.buyeruid).to.equal('d966c5be-c49f-4f73-8cd1-37b6b5790653-tuct9f7bf10'); }); it('should get user id from TRC if local storage and cookie isn`t defined', function () { @@ -408,8 +499,7 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(window.TRC.user_id); + expect(res.data.user.buyeruid).to.equal(window.TRC.user_id); delete window.TRC; }); @@ -422,8 +512,7 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(0); + expect(res.data.user.buyeruid).to.equal(0); }); it('should set buyeruid to be 0 if it`s a new user', function () { @@ -431,13 +520,29 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(0); + expect(res.data.user.buyeruid).to.equal(0); }); }); }) describe('interpretResponse', function () { + const defaultBidRequest = { + ...createBidRequest(), + ...displayBidRequestParams, + }; + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + const bidderRequest = { + ...commonBidderRequest + }; + const request = spec.buildRequests([defaultBidRequest], bidderRequest); + const serverResponse = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -446,7 +551,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': request.data.imp[0].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', @@ -471,15 +576,6 @@ describe('Taboola Adapter', function () { } }; - const request = { - bids: [ - { - ...commonBidRequest, - ...displayBidRequestParams - } - ] - } - it('should return empty array if no valid bids', function () { const res = spec.interpretResponse(serverResponse, []) expect(res).to.be.an('array').that.is.empty @@ -513,18 +609,7 @@ describe('Taboola Adapter', function () { }); it('should interpret multi impression request', function () { - const multiRequest = { - bids: [ - { - ...createBidRequest(), - ...displayBidRequestParams - }, - { - ...createBidRequest(), - ...displayBidRequestParams - } - ] - } + const multiRequest = spec.buildRequests([defaultBidRequest, defaultBidRequest], bidderRequest); const multiServerResponse = { body: { @@ -534,7 +619,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '2', + 'impid': multiRequest.data.imp[0].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': 'ADM2', @@ -551,7 +636,7 @@ describe('Taboola Adapter', function () { }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': multiRequest.data.imp[1].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': 'ADM1', @@ -582,6 +667,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[1].bidId, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponse.body.seatbid[0].bid[0].id, ttl: 60, netRevenue: true, currency: multiServerResponse.body.cur, @@ -598,6 +685,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[0].bidId, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponse.body.seatbid[0].bid[1].id, ttl: 60, netRevenue: true, currency: multiServerResponse.body.cur, @@ -621,8 +710,10 @@ describe('Taboola Adapter', function () { const expectedRes = [ { requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, ttl: 60, netRevenue: true, currency: serverResponse.body.cur, @@ -648,8 +739,10 @@ describe('Taboola Adapter', function () { const expectedRes = [ { requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, ttl: 125, netRevenue: true, currency: serverResponse.body.cur, @@ -668,18 +761,7 @@ describe('Taboola Adapter', function () { }); it('should replace AUCTION_PRICE macro in adm', function () { - const multiRequest = { - bids: [ - { - ...createBidRequest(), - ...displayBidRequestParams - }, - { - ...createBidRequest(), - ...displayBidRequestParams - } - ] - } + const multiRequest = spec.buildRequests([defaultBidRequest, defaultBidRequest], bidderRequest); const multiServerResponseWithMacro = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -688,7 +770,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '2', + 'impid': multiRequest.data.imp[0].id, 'price': 0.34, 'adid': '2785119545551083381', 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', @@ -705,7 +787,7 @@ describe('Taboola Adapter', function () { }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': multiRequest.data.imp[1].id, 'price': 0.35, 'adid': '2785119545551083381', 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', @@ -735,6 +817,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[1].bidId, cpm: multiServerResponseWithMacro.body.seatbid[0].bid[0].price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponseWithMacro.body.seatbid[0].bid[0].id, ttl: 60, netRevenue: true, currency: multiServerResponseWithMacro.body.cur, @@ -751,6 +835,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[0].bidId, cpm: multiServerResponseWithMacro.body.seatbid[0].bid[1].price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponseWithMacro.body.seatbid[0].bid[1].id, ttl: 60, netRevenue: true, currency: multiServerResponseWithMacro.body.cur, @@ -771,17 +857,28 @@ describe('Taboola Adapter', function () { describe('getUserSyncs', function () { const usersyncUrl = 'https://trc.taboola.com/sg/prebidJS/1/cm'; + const iframeUrl = 'https://cdn.taboola.com/scripts/prebid_iframe_sync.html'; - it('should not return user sync if pixelEnabled is false', function () { - const res = spec.getUserSyncs({pixelEnabled: false}); + it('should not return user sync if pixelEnabled is false and iframe disabled', function () { + const res = spec.getUserSyncs({pixelEnabled: false, iframeEnabled: false}); expect(res).to.be.an('array').that.is.empty; }); it('should return user sync if pixelEnabled is true', function () { - const res = spec.getUserSyncs({pixelEnabled: true}); + const res = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: false}); expect(res).to.deep.equal([{type: 'image', url: usersyncUrl}]); }); + it('should return user sync if iframeEnabled is true', function () { + const res = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}); + expect(res).to.deep.equal([{type: 'iframe', url: iframeUrl}]); + }); + + it('should return both user syncs if iframeEnabled is true and pixelEnabled is true', function () { + const res = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}); + expect(res).to.deep.equal([{type: 'iframe', url: iframeUrl}, {type: 'image', url: usersyncUrl}]); + }); + it('should pass consent tokens values', function() { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'GDPR_CONSENT'}, 'USP_CONSENT')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT` diff --git a/test/spec/modules/tagorasBidAdapter_spec.js b/test/spec/modules/tagorasBidAdapter_spec.js new file mode 100644 index 00000000000..7559567dcff --- /dev/null +++ b/test/spec/modules/tagorasBidAdapter_spec.js @@ -0,0 +1,651 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/tagorasBidAdapter'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppString': 'gpp_string', + 'gppSid': [7], + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } + } +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['tagoras.io'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('TagorasBidAdapter', function () { + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + tagoras: { + storageAllowed: true + } + }; + sandbox = sinon.sandbox.create(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + prebidVersion: version, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + }, + gpid: '' + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + gpid: '0123456789', + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.tagoras.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.tagoras.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.tagoras.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }); + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.tagoras.io/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['tagoras.io'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['tagoras.io'], + agencyName: 'Agency Name' + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['tagoras.io'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'lipb': + return {lipbid: id}; + case 'parrableId': + return {eid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + tagoras: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + tagoras: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem('myKey', 2020); + const {value, created} = getStorageItem('myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem('myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 9bd8c44b88a..98f706f5193 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -1,23 +1,21 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/teadsBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; +import { off } from '../../../src/events'; const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; describe('teadsBidAdapter', () => { const adapter = newBidder(spec); - let cookiesAreEnabledStub, getCookieStub; + let sandbox; beforeEach(function () { - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); + sandbox = sinon.sandbox.create(); }); afterEach(function () { - cookiesAreEnabledStub.restore(); - getCookieStub.restore(); + sandbox.restore(); }); describe('inherited functions', () => { @@ -257,20 +255,156 @@ describe('teadsBidAdapter', () => { expect(payload.pageReferrer).to.deep.equal(document.referrer); }); - it('should add pageTitle info to payload', function () { + it('should add screenOrientation info to payload', function () { const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); + const screenOrientation = window.top.screen.orientation?.type - expect(payload.pageTitle).to.exist; - expect(payload.pageTitle).to.deep.equal(window.top.document.title || document.title); + if (screenOrientation) { + expect(payload.screenOrientation).to.exist; + expect(payload.screenOrientation).to.deep.equal(screenOrientation); + } else expect(payload.screenOrientation).to.not.exist; }); - it('should add pageDescription info to payload', function () { + it('should add historyLength info to payload', function () { const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); - expect(payload.pageDescription).to.exist; - expect(payload.pageDescription).to.deep.equal(''); + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add hardwareConcurrency info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const hardwareConcurrency = window.top.navigator?.hardwareConcurrency + + if (hardwareConcurrency) { + expect(payload.hardwareConcurrency).to.exist; + expect(payload.hardwareConcurrency).to.deep.equal(hardwareConcurrency); + } else expect(payload.hardwareConcurrency).to.not.exist + }); + + it('should add deviceMemory info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceMemory = window.top.navigator.deviceMemory + + if (deviceMemory) { + expect(payload.deviceMemory).to.exist; + expect(payload.deviceMemory).to.deep.equal(deviceMemory); + } else expect(payload.deviceMemory).to.not.exist; + }); + + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); }); it('should add timeToFirstByte info to payload', function () { @@ -697,7 +831,7 @@ describe('teadsBidAdapter', () => { describe('First-party cookie Teads ID', function () { it('should not add firstPartyCookieTeadsId param to payload if cookies are not enabled' + ' and teads user id not available', function () { - cookiesAreEnabledStub.returns(false); + sandbox.stub(storage, 'cookiesAreEnabled').returns(false); const bidRequest = { ...baseBidRequest, @@ -714,8 +848,8 @@ describe('teadsBidAdapter', () => { it('should not add firstPartyCookieTeadsId param to payload if cookies are enabled ' + 'but first-party cookie and teads user id are not available', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns(undefined); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns(undefined); const bidRequest = { ...baseBidRequest, @@ -732,8 +866,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from cookie if it\'s available ' + 'and teads user id is not', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -751,8 +885,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from user id module if it\'s available ' + 'even if cookie is available too', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, diff --git a/test/spec/modules/theAdxBidAdapter_spec.js b/test/spec/modules/theAdxBidAdapter_spec.js index 99e5156190c..eb00834421a 100644 --- a/test/spec/modules/theAdxBidAdapter_spec.js +++ b/test/spec/modules/theAdxBidAdapter_spec.js @@ -446,6 +446,78 @@ describe('TheAdxAdapter', function () { expect(processedBid.currency).to.equal(responseCurrency); }); + it('returns a valid deal bid response on sucessful banner request with deal', function () { + let incomingRequestId = 'XXtestingXX'; + let responsePrice = 3.14 + + let responseCreative = 'sample_creative&{FOR_COVARAGE}'; + + let responseCreativeId = '274'; + let responseCurrency = 'TRY'; + + let responseWidth = 300; + let responseHeight = 250; + let responseTtl = 213; + let dealId = 'theadx_deal_id'; + + let sampleResponse = { + id: '66043f5ca44ecd8f8769093b1615b2d9', + seatbid: [{ + bid: [{ + id: 'c21bab0e-7668-4d8f-908a-63e094c09197', + dealid: 'theadx_deal_id', + impid: '1', + price: responsePrice, + adid: responseCreativeId, + crid: responseCreativeId, + adm: responseCreative, + adomain: [ + 'www.domain.com' + ], + cid: '274', + attr: [], + w: responseWidth, + h: responseHeight, + ext: { + ttl: responseTtl + } + }], + seat: '201', + group: 0 + }], + bidid: 'c21bab0e-7668-4d8f-908a-63e094c09197', + cur: responseCurrency + }; + + let sampleRequest = { + bidId: incomingRequestId, + mediaTypes: { + banner: {} + }, + requestId: incomingRequestId, + deals: [{id: dealId}] + }; + let serverResponse = { + body: sampleResponse + } + let result = spec.interpretResponse(serverResponse, sampleRequest); + + expect(result.length).to.equal(1); + + let processedBid = result[0]; + + // expect(processedBid.requestId).to.equal(incomingRequestId); + expect(processedBid.cpm).to.equal(responsePrice); + expect(processedBid.width).to.equal(responseWidth); + expect(processedBid.height).to.equal(responseHeight); + expect(processedBid.ad).to.equal(responseCreative); + expect(processedBid.ttl).to.equal(responseTtl); + expect(processedBid.creativeId).to.equal(responseCreativeId); + expect(processedBid.netRevenue).to.equal(true); + expect(processedBid.currency).to.equal(responseCurrency); + expect(processedBid.dealId).to.equal(dealId); + }); + it('returns an valid bid response on sucessful video request', function () { let incomingRequestId = 'XXtesting-275XX'; let responsePrice = 6 diff --git a/test/spec/modules/tpmnBidAdapter_spec.js b/test/spec/modules/tpmnBidAdapter_spec.js index e2b14b18f61..505bc9d878f 100644 --- a/test/spec/modules/tpmnBidAdapter_spec.js +++ b/test/spec/modules/tpmnBidAdapter_spec.js @@ -1,16 +1,130 @@ /* eslint-disable no-tabs */ -import {expect} from 'chai'; -import {spec, storage} from 'modules/tpmnBidAdapter.js'; -import {generateUUID} from '../../../src/utils.js'; -import {newBidder} from '../../../src/adapters/bidderFactory'; +import { spec, storage, VIDEO_RENDERER_URL, ADAPTER_VERSION } from 'modules/tpmnBidAdapter.js'; +import { generateUUID } from '../../../src/utils.js'; +import { expect } from 'chai'; +import * as utils from 'src/utils'; import * as sinon from 'sinon'; +import 'modules/consentManagement.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {mockGdprConsent} from '../../helpers/consentData.js'; + +const BIDDER_CODE = 'tpmn'; +const BANNER_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const VIDEO_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + video: { + context: 'outstream', + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + playerSize: [[1024, 768]], + protocols: [3, 4, 7, 8, 10], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const BIDDER_REQUEST = { + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + bidderRequestId: 'bidderRequestId', + timeout: 500, + refererInfo: { + page: 'https://hello-world-page.com/', + domain: 'hello-world-page.com', + ref: 'http://example-domain.com/foo', + } +}; + +const BANNER_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidId': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'w': 300, + 'h': 250 + } + ] + } + ], + 'cur': 'USD' +}; + +const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 1.09, + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 768, + 'w': 1024 + } + ] + } + ], + 'cur': 'USD' +}; describe('tpmnAdapterTests', function () { - const adapter = newBidder(spec); - const BIDDER_CODE = 'tpmn'; let sandbox = sinon.sandbox.create(); let getCookieStub; - beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { tpmn: { @@ -27,152 +141,277 @@ describe('tpmnAdapterTests', function () { $$PREBID_GLOBAL$$.bidderSettings = {}; }); - describe('inherited functions', function () { - it('exists and is a function', function () { - expect(adapter.callBids).to.exist.and.to.be.a('function') - }) - }); - - describe('isBidRequestValid', function () { - let bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - } - }; - - it('should return true if a bid is valid banner bid request', function () { - expect(spec.isBidRequestValid(bid)).to.be.equal(true); - }); - - it('should return false where requried param is missing', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - - it('should return false when required param values have invalid type', function () { - let bid = Object.assign({}, bid); - bid.params = { - 'inventoryId': null, - 'publisherId': null - }; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - }); - - describe('buildRequests', function () { - it('should return an empty list if there are no bid requests', function () { - const emptyBidRequests = []; - const bidderRequest = {}; - const request = spec.buildRequests(emptyBidRequests, bidderRequest); - expect(request).to.be.an('array').that.is.empty; - }); - it('should generate a POST server request with bidder API url, data', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', + describe('isBidRequestValid()', function () { + it('should accept request if placementId is passed', function () { + let bid = { + bidder: BIDDER_CODE, params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 123 }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', mediaTypes: { banner: { sizes: [[300, 250]] } } }; - const tempBidRequests = [bid]; - const tempBidderRequest = { - refererInfo: { - page: 'http://localhost/test', - site: { - domain: 'localhost', - page: 'http://localhost/test' - } - } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should reject requests without params', function () { + let bid = { + bidder: BIDDER_CODE, + params: {} }; - const builtRequest = spec.buildRequests(tempBidRequests, tempBidderRequest); - - expect(builtRequest).to.have.lengthOf(1); - expect(builtRequest[0].method).to.equal('POST'); - expect(builtRequest[0].url).to.match(/ad.tpmn.co.kr\/prebidhb.tpmn/); - const apiRequest = builtRequest[0].data; - expect(apiRequest.site).to.deep.equal({ - domain: 'localhost', - page: 'http://localhost/test' - }); - expect(apiRequest.bids).to.have.lengthOf('1'); - expect(apiRequest.bids[0].inventoryId).to.equal('1'); - expect(apiRequest.bids[0].publisherId).to.equal('TPMN'); - expect(apiRequest.bids[0].bidId).to.equal('29092404798c9'); - expect(apiRequest.bids[0].adUnitCode).to.equal('temp-unitcode'); - expect(apiRequest.bids[0].auctionId).to.equal('da1d7a33-0260-4e83-a621-14674116f3f9'); - expect(apiRequest.bids[0].sizes).to.have.lengthOf('1'); - expect(apiRequest.bids[0].sizes[0]).to.deep.equal({ - width: 300, - height: 250 - }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(BANNER_BID)).to.equal(true); + expect(spec.isBidRequestValid(VIDEO_BID)).to.equal(true); }); }); - describe('interpretResponse', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] + describe('buildRequests()', function () { + it('should have gdpr data if applicable', function () { + const bid = utils.deepClone(BANNER_BID); + + const req = syncAddFPDToBidderRequest(Object.assign({}, BIDDER_REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, } + })); + let request = spec.buildRequests([bid], req)[0]; + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + + it('should properly forward ORTB blocking params', function () { + let bid = utils.deepClone(BANNER_BID); + bid = utils.mergeDeep(bid, { + params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'], battr: [1] }, + mediaTypes: { banner: { battr: [1] } } + }); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + + expect(request).to.exist.and.to.be.an('object'); + const payload = request.data; + expect(payload).to.have.deep.property('bcat', ['IAB1-1']); + expect(payload).to.have.deep.property('badv', ['example.com']); + expect(payload).to.have.deep.property('bapp', ['com.example']); + expect(payload.imp[0].banner).to.have.deep.property('battr', [1]); + }); + + context('when mediaType is banner', function () { + it('should build correct request for banner bid with both w, h', () => { + const bid = utils.deepClone(BANNER_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const requestData = request.data; + // expect(requestData.imp[0].banner).to.equal(null); + expect(requestData.imp[0].banner.format[0].w).to.equal(300); + expect(requestData.imp[0].banner.format[0].h).to.equal(250); + }); + + it('should create request data', function () { + const bid = utils.deepClone(BANNER_BID); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bid.bidId); + }); + }); + + context('when mediaType is video', function () { + if (FEATURES.VIDEO) { + it('should return false when there is no video in mediaTypes', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); } - }; - const tempBidRequests = [bid]; - it('should return an empty aray to indicate no valid bids', function () { - const emptyServerResponse = {}; - const bidResponses = spec.interpretResponse(emptyServerResponse, tempBidRequests); - expect(bidResponses).is.an('array').that.is.empty; + if (FEATURES.VIDEO) { + it('should reutrn false if player size is not set', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + } + if (FEATURES.VIDEO) { + it('when mediaType is Video - check', () => { + const bid = utils.deepClone(VIDEO_BID); + const check = { + w: 1024, + h: 768, + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + api: [1, 2, 4, 6], + protocols: [3, 4, 7, 8, 10], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0, + plcmt: 1 + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } + + if (FEATURES.VIDEO) { + it('when mediaType New Video', () => { + const NEW_VIDEO_BID = { + 'bidder': 'tpmn', + 'params': {'inventoryId': 2, 'bidFloor': 2}, + 'userId': {'pubcid': '88a49ee6-beeb-4dd6-92ac-3b6060e127e1'}, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': ['video/mp4'], + 'playerSize': [[1024, 768]], + 'playbackmethod': [2, 4, 6], + 'protocols': [3, 4], + 'api': [1, 2, 3, 6], + 'placement': 1, + 'minduration': 0, + 'maxduration': 30, + 'startdelay': 0, + 'skip': 1, + 'plcmt': 4 + } + }, + }; + + const check = { + w: 1024, + h: 768, + mimes: [ 'video/mp4' ], + playbackmethod: [2, 4, 6], + api: [1, 2, 3, 6], + protocols: [3, 4], + placement: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + skip: 1, + plcmt: 4 + } + + expect(spec.isBidRequestValid(NEW_VIDEO_BID)).to.equal(true); + let requests = spec.buildRequests([NEW_VIDEO_BID], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video.w).to.equal(check.w); + expect(request.imp[0].video.h).to.equal(check.h); + expect(request.imp[0].video.placement).to.equal(check.placement); + expect(request.imp[0].video.minduration).to.equal(check.minduration); + expect(request.imp[0].video.maxduration).to.equal(check.maxduration); + expect(request.imp[0].video.startdelay).to.equal(check.startdelay); + expect(request.imp[0].video.skip).to.equal(check.skip); + expect(request.imp[0].video.plcmt).to.equal(check.plcmt); + expect(request.imp[0].video.mimes).to.deep.have.same.members(check.mimes); + expect(request.imp[0].video.playbackmethod).to.deep.have.same.members(check.playbackmethod); + expect(request.imp[0].video.api).to.deep.have.same.members(check.api); + expect(request.imp[0].video.protocols).to.deep.have.same.members(check.protocols); + }); + } + + if (FEATURES.VIDEO) { + it('should use bidder video params if they are set', () => { + let bid = utils.deepClone(VIDEO_BID); + const check = { + api: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [3, 4], + protocols: [5, 6], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + w: 640, + h: 480 + + }; + bid.mediaTypes.video = {...check}; + bid.mediaTypes.video.context = 'instream'; + bid.mediaTypes.video.playerSize = [[640, 480]]; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } }); - it('should return an empty array to indicate no valid bids', function () { - const mockBidResult = { - requestId: '9cf19229-34f6-4d06-bc1d-0e44e8d616c8', - cpm: 10.0, - creativeId: '1', - width: 300, - height: 250, - netRevenue: true, - currency: 'USD', - ttl: 1800, - ad: '', - adType: 'banner' - }; - const testServerResponse = { - headers: [], - body: [mockBidResult] - }; - const bidResponses = spec.interpretResponse(testServerResponse, tempBidRequests); - expect(bidResponses).deep.equal([mockBidResult]); + }); + + describe('interpretResponse()', function () { + context('when mediaType is banner', function () { + it('should correctly interpret valid banner response', function () { + const bid = utils.deepClone(BANNER_BID); + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const response = utils.deepClone(BANNER_BID_RESPONSE); + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[0].burl).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].ad).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].creativeId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + + it('should handle empty bid response', function () { + const bid = utils.deepClone(BANNER_BID); + + let request = spec.buildRequests([bid], BIDDER_REQUEST)[0]; + const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); }); + if (FEATURES.VIDEO) { + context('when mediaType is video', function () { + it('should correctly interpret valid instream video response', () => { + const bid = utils.deepClone(VIDEO_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].burl).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].vastXml).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].rendererUrl).to.equal(VIDEO_RENDERER_URL); + expect(bids[0].creativeId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + }); + } }); describe('getUserSync', function () { diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 56c506dea6b..1fe504ba8e8 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -262,6 +262,11 @@ describe('ttdBidAdapter', function () { expect(request.data).to.be.not.null; }); + it('sets bidrequest.id to bidderRequestId', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.id).to.equal('18084284054531'); + }); + it('sets impression id to ad unit\'s bid id', function () { const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; expect(requestBody.imp[0].id).to.equal('243310435309b5'); diff --git a/test/spec/modules/ucfunnelBidAdapter_spec.js b/test/spec/modules/ucfunnelBidAdapter_spec.js index 9bec7229450..998e0db6fe8 100644 --- a/test/spec/modules/ucfunnelBidAdapter_spec.js +++ b/test/spec/modules/ucfunnelBidAdapter_spec.js @@ -30,7 +30,7 @@ const validBannerBidReq = { params: { adid: 'ad-34BBD2AA24B678BBFD4E7B9EE3B872D' }, - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746', ortb2Imp: { @@ -180,15 +180,15 @@ describe('ucfunnel Adapter', function () { expect(data.schain).to.equal('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com'); }); - it('must parse bid size from a nested array', function () { - const width = 640; - const height = 480; - const bid = deepClone(validBannerBidReq); - bid.sizes = [[ width, height ]]; - const requests = spec.buildRequests([ bid ], bidderRequest); + it('should support multiple size', function () { + const sizes = [[300, 250], [336, 280]]; + const format = '300,250;336,280'; + validBannerBidReq.sizes = sizes; + const requests = spec.buildRequests([ validBannerBidReq ], bidderRequest); const data = requests[0].data; - expect(data.w).to.equal(width); - expect(data.h).to.equal(height); + expect(data.w).to.equal(sizes[0][0]); + expect(data.h).to.equal(sizes[0][1]); + expect(data.format).to.equal(format); }); it('should set bidfloor if configured', function() { diff --git a/test/spec/modules/uid2IdSystem_helpers.js b/test/spec/modules/uid2IdSystem_helpers.js index 5006a50dedd..e0bef047acb 100644 --- a/test/spec/modules/uid2IdSystem_helpers.js +++ b/test/spec/modules/uid2IdSystem_helpers.js @@ -26,12 +26,12 @@ export const runAuction = async () => { } export const apiHelpers = { - makeTokenResponse: (token, shouldRefresh = false, expired = false) => ({ + makeTokenResponse: (token, shouldRefresh = false, expired = false, refreshExpired = false) => ({ advertising_token: token, refresh_token: 'fake-refresh-token', identity_expires: expired ? Date.now() - 1000 : Date.now() + 60 * 60 * 1000, refresh_from: shouldRefresh ? Date.now() - 1000 : Date.now() + 60 * 1000, - refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + refresh_expires: refreshExpired ? Date.now() - 1000 : Date.now() + 24 * 60 * 60 * 1000, // 24 hours refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', // Fake data }), respondAfterDelay: (delay, srv = server) => new Promise((resolve) => setTimeout(() => { diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index f33060869df..901e0c57e32 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; @@ -11,6 +11,7 @@ import { configureTimerInterceptors } from 'test/mocks/timers.js'; import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -23,16 +24,22 @@ const auctionDelayMs = 10; const initialToken = `initial-advertising-token`; const legacyToken = 'legacy-advertising-token'; const refreshedToken = 'refreshed-advertising-token'; +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; const legacyConfigParams = {storage: null}; const serverCookieConfigParams = { uid2ServerCookie: publisherCookieName }; const newServerCookieConfigParams = { uid2Cookie: publisherCookieName }; +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } const makeUid2IdentityContainer = (token) => ({uid2: {id: token}}); let useLocalStorage = false; const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'uid2', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}}] }, debug, ...extraSettings }); +const makeOriginalIdentity = (identity, salt = 1) => ({ + identity: utils.cyrb53Hash(identity, salt), + salt +}) const getFromAppropriateStorage = () => { if (useLocalStorage) return coreStorage.getDataFromLocalStorage(moduleCookieName); @@ -46,15 +53,18 @@ const expectGlobalToHaveToken = (token) => expect(getGlobal().getUserIds()).to.d const expectGlobalToHaveNoUid2 = () => expect(getGlobal().getUserIds()).to.not.haveOwnProperty('uid2'); const expectNoLegacyToken = (bid) => expect(bid.userId).to.not.deep.include(makeUid2IdentityContainer(legacyToken)); const expectModuleStorageEmptyOrMissing = () => expect(getFromAppropriateStorage()).to.be.null; -const expectModuleStorageToContain = (initialIdentity, latestIdentity) => { +const expectModuleStorageToContain = (originalAdvertisingToken, latestAdvertisingToken, originalIdentity) => { const cookie = JSON.parse(getFromAppropriateStorage()); - if (initialIdentity) expect(cookie.originalToken.advertising_token).to.equal(initialIdentity); - if (latestIdentity) expect(cookie.latestToken.advertising_token).to.equal(latestIdentity); + if (originalAdvertisingToken) expect(cookie.originalToken.advertising_token).to.equal(originalAdvertisingToken); + if (latestAdvertisingToken) expect(cookie.latestToken.advertising_token).to.equal(latestAdvertisingToken); + if (originalIdentity) expect(cookie.originalIdentity).to.eql(makeOriginalIdentity(Object.values(originalIdentity)[0], cookie.originalIdentity.salt)); } -const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; +const apiUrl = 'https://prod.uidapi.com/v2/token' +const refreshApiUrl = `${apiUrl}/refresh`; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); +const makeSuccessResponseBody = (responseToken) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: responseToken } })); +const cstgApiUrl = `${apiUrl}/client-generate`; const testCookieAndLocalStorage = (description, test, only = false) => { const describeFn = only ? describe.only : describe; @@ -76,7 +86,7 @@ const testCookieAndLocalStorage = (description, test, only = false) => { }; describe(`UID2 module`, function () { - let server, suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; before(function () { timerSpy = configureTimerInterceptors(debugOutput); hook.ready(); @@ -87,10 +97,18 @@ describe(`UID2 module`, function () { // I've confirmed it's available in Firefox since v34 (it seems to be unavailable on BrowserStack in Firefox v106). if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function () { @@ -99,18 +117,18 @@ describe(`UID2 module`, function () { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); - const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); - const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); - const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); + const configureUid2Response = (apiUrl, httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureUid2ApiSuccessResponse = (apiUrl, responseToken) => configureUid2Response(apiUrl, 200, makeSuccessResponseBody(responseToken)); + const configureUid2ApiFailResponse = (apiUrl) => configureUid2Response(apiUrl, 500, 'Error'); // Runs the provided test twice - once with a successful API mock, once with one which returns a server error - const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { + const testApiSuccessAndFailure = (act, apiUrl, testDescription, failTestDescription, only = false, responseToken = refreshedToken) => { const testFn = only ? it.only : it; testFn(`API responds successfully: ${testDescription}`, async function() { - configureUid2ApiSuccessResponse(); + configureUid2ApiSuccessResponse(apiUrl, responseToken); await act(true); }); testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { - configureUid2ApiFailResponse(); + configureUid2ApiFailResponse(apiUrl); await act(false); }); } @@ -123,8 +141,6 @@ describe(`UID2 module`, function () { debugOutput(fullTestTitle); testSandbox = sinon.sandbox.create(); testSandbox.stub(utils, 'logWarn'); - server = sinon.createFakeServer(); - init(config); setSubmoduleRegistry([uid2IdSubmodule]); }); @@ -151,13 +167,13 @@ describe(`UID2 module`, function () { it('When no baseUrl is provided in config, the module calls the production endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token})); - expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/v2/token/refresh'); }); it('When a baseUrl is provided in config, the module calls the provided endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token, uid2ApiBase: 'https://operator-integ.uidapi.com'})); - expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/v2/token/refresh'); }); }); @@ -238,7 +254,7 @@ describe(`UID2 module`, function () { cookieHelpers.setPublisherCookie(publisherCookieName, token); config.setConfig(makePrebidConfig(serverCookieConfigParams, extraConfig)); }, - } + }, ] scenarios.forEach(function(scenario) { @@ -252,7 +268,7 @@ describe(`UID2 module`, function () { if (apiSucceeds) expectToken(bid, refreshedToken); else expectNoIdentity(bid); - }, 'it should be used in the auction', 'the auction should have no uid2'); + }, refreshApiUrl, 'it should be used in the auction', 'the auction should have no uid2'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); @@ -264,14 +280,14 @@ describe(`UID2 module`, function () { } else { expectModuleStorageEmptyOrMissing(); } - }, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); + }, refreshApiUrl, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); }); describe(`when the response doesn't arrive before the auction timer`, function() { testApiSuccessAndFailure(async function() { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); const bid = await runAuction(); expectNoIdentity(bid); - }, 'it should run the auction'); + }, refreshApiUrl, 'it should run the auction'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); @@ -283,7 +299,7 @@ describe(`UID2 module`, function () { await promise; if (apiSucceeds) expectGlobalToHaveToken(refreshedToken); else expectGlobalToHaveNoUid2(); - }, 'it should update the userId after the auction', 'there should be no global identity'); + }, refreshApiUrl, 'it should update the userId after the auction', 'there should be no global identity'); }) describe('and there is a refreshed token in the module cookie', function() { it('the refreshed value from the cookie is used', async function() { @@ -322,7 +338,7 @@ describe(`UID2 module`, function () { apiHelpers.respondAfterDelay(10, server); const bid = await runAuction(); expectToken(bid, initialToken); - }, 'it should not be refreshed before the auction runs'); + }, refreshApiUrl, 'it should not be refreshed before the auction runs'); testApiSuccessAndFailure(async function(success) { const promise = apiHelpers.respondAfterDelay(1, server); @@ -333,7 +349,7 @@ describe(`UID2 module`, function () { } else { expectModuleStorageToContain(initialToken, initialToken); } - }, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); + }, refreshApiUrl, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); it('it should use the current token in the auction', async function() { const bid = await runAuction(); @@ -342,4 +358,273 @@ describe(`UID2 module`, function () { }); }); }); + + if (FEATURES.UID2_CSTG) { + describe('When CSTG is enabled provided', function () { + const scenarios = [ + { + name: 'email provided in config', + identity: { email: 'test@example.com' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test . test@gmail.com' }, extraConfig)) + }, + { + name: 'phone provided in config', + identity: { phone: '+12345678910' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phone: 'test123' }, extraConfig)) + }, + { + name: 'email hash provided in config', + identity: { email_hash: 'lz3+Rj7IV4X1+Vr1ujkG7tstkxwk5pgkqJ6mXbpOgTs=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: this.identity.email_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: 'test@example.com' }, extraConfig)) + }, + { + name: 'phone hash provided in config', + identity: { phone_hash: 'kVJ+4ilhrqm3HZDDnCQy4niZknvCoM4MkoVzZrQSdJw=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: this.identity.phone_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: '614332222111' }, extraConfig)) + }, + ] + scenarios.forEach(function(scenario) { + describe(`And ${scenario.name}`, function() { + describe(`When invalid identity is provided`, function() { + it('the auction should have no uid2', async function () { + scenario.setInvalidConfig() + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + }); + + describe('When valid identity is provided, and the auction is set to run immediately', function() { + it('it should ignores token provided in config, and the auction should have no uid2', async function() { + scenario.setConfig({ uid2Token: apiHelpers.makeTokenResponse(initialToken), auctionDelay: 0, syncDelay: 1 }); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + it('it should ignores token provided in server-set cookie', async function() { + cookieHelpers.setPublisherCookie(publisherCookieName, initialToken); + scenario.setConfig({ ...newServerCookieConfigParams, auctionDelay: 0, syncDelay: 1 }) + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + describe('When the token generated in time', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should be used in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, scenario.identity); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + }); + describe(`when the response doesn't arrive before the auction timer`, function() { + testApiSuccessAndFailure(async function() { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }, cstgApiUrl, 'it should run the auction', undefined, false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); + + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + await promise; + if (apiSucceeds) expectGlobalToHaveToken(clientSideGeneratedToken); + else expectGlobalToHaveNoUid2(); + }, cstgApiUrl, 'it should update the userId after the auction', 'there should be no global identity', false, clientSideGeneratedToken); + }) + + describe('when there is a token in the module cookie', function() { + describe('when originalIdentity matches', function() { + describe('When the storedToken is valid', function() { + it('it should use the stored token in the auction', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com', auctionDelay: 0, syncDelay: 1 })); + const bid = await runAuction(); + expectToken(bid, refreshedToken); + }); + }) + + describe('When the storedToken is expired and can be refreshed ', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, refreshedToken); + else expectNoIdentity(bid); + }, refreshApiUrl, 'it should use refreshed token in the auction', 'the auction should have no uid2'); + }) + + describe('When the storedToken is expired for refresh', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should use generated token in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + }) + }) + + it('when originalIdentity not match, the auction should has no uid2', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }); + }) + }); + describe('When invalid CSTG configuration is provided', function () { + const invalidConfigs = [ + { + name: 'CSTG option is not a object', + cstgOptions: '' + }, + { + name: 'CSTG option is null', + cstgOptions: '' + }, + { + name: 'serverPublicKey is not a string', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: {} } + }, + { + name: 'serverPublicKey not match regular expression', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: 'serverPublicKey' } + }, + { + name: 'subscriptionId is not a string', + cstgOptions: { subscriptionId: {}, serverPublicKey: cstgConfigParams.serverPublicKey } + }, + { + name: 'subscriptionId is empty', + cstgOptions: { subscriptionId: '', serverPublicKey: cstgConfigParams.serverPublicKey } + }, + ] + invalidConfigs.forEach(function(scenario) { + describe(`When ${scenario.name}`, function() { + it('should not generate token using identity', async () => { + config.setConfig(makePrebidConfig({ ...scenario.cstgOptions, email: 'test@email.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }); + }); + }); + }); + describe('When email is provided in different format', function () { + const testCases = [ + { originalEmail: 'TEst.TEST@Test.com ', normalizedEmail: 'test.test@test.com' }, + { originalEmail: 'test+test@test.com', normalizedEmail: 'test+test@test.com' }, + { originalEmail: ' testtest@test.com ', normalizedEmail: 'testtest@test.com' }, + { originalEmail: 'TEst.TEst+123@GMail.Com', normalizedEmail: 'testtest@gmail.com' } + ]; + testCases.forEach((testCase) => { + describe('it should normalize the email and generate token on normalized email', async () => { + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: testCase.originalEmail })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, { email: testCase.normalizedEmail }); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + } + + describe('When neither token nor CSTG config provided', function () { + describe('when there is a non-cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + describe('when there is a cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + it('the auction should has no uid2', async function () { + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) }); diff --git a/test/spec/modules/underdogmediaBidAdapter_spec.js b/test/spec/modules/underdogmediaBidAdapter_spec.js index 2d7c1f11178..c0e2e8dddce 100644 --- a/test/spec/modules/underdogmediaBidAdapter_spec.js +++ b/test/spec/modules/underdogmediaBidAdapter_spec.js @@ -5,6 +5,7 @@ import { spec, resetUserSync } from 'modules/underdogmediaBidAdapter.js'; +import { config } from '../../../src/config'; describe('UnderdogMedia adapter', function () { let bidRequests; @@ -763,6 +764,20 @@ describe('UnderdogMedia adapter', function () { expect(request.data.ref).to.equal(undefined); }); + it('should have pbTimeout to be 3001 if bidder timeout does not exists', function () { + config.setConfig({ bidderTimeout: '' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(3001) + }) + + it('should have pbTimeout to be a numerical value if bidder timeout is in a string', function () { + config.setConfig({ bidderTimeout: '1000' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(1000) + }) + it('should have pubcid if it exists', function () { let bidRequests = [{ adUnitCode: 'div-gpt-ad-1460505748561-0', diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 7f9c2e7b3d5..5cf53c661a9 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -39,12 +39,19 @@ const videoBidReq = [{ maxDuration: 30 } }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480], - placement: 1, - plcmt: 1 - }}, + ortb2Imp: { + ext: { + gpid: '/1111/gpid#728x90', + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 1, + plcmt: 1 + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -56,10 +63,19 @@ const videoBidReq = [{ placementId: '10433395', publisherId: 12345 }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480] - }}, + ortb2Imp: { + ext: { + data: { + pbadslot: '/1111/pbadslot#728x90' + } + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -463,12 +479,14 @@ describe('Undertone Adapter', () => { expect(bidVideo.video.skippable).to.equal(true); expect(bidVideo.video.placement).to.equal(1); expect(bidVideo.video.plcmt).to.equal(1); + expect(bidVideo.gpid).to.equal('/1111/gpid#728x90'); expect(bidVideo2.video.skippable).to.equal(null); expect(bidVideo2.video.maxDuration).to.equal(null); expect(bidVideo2.video.playbackMethod).to.equal(null); expect(bidVideo2.video.placement).to.equal(null); expect(bidVideo2.video.plcmt).to.equal(null); + expect(bidVideo2.gpid).to.equal('/1111/pbadslot#728x90'); }); it('should send all userIds data to server', function () { const request = spec.buildRequests(bidReqUserIds, bidderReq); diff --git a/test/spec/modules/unicornBidAdapter_spec.js b/test/spec/modules/unicornBidAdapter_spec.js index 0abb09bfb78..bd9175dac1e 100644 --- a/test/spec/modules/unicornBidAdapter_spec.js +++ b/test/spec/modules/unicornBidAdapter_spec.js @@ -1,4 +1,5 @@ import {assert, expect} from 'chai'; +import * as utils from 'src/utils.js'; import {spec} from 'modules/unicornBidAdapter.js'; import * as _ from 'lodash'; @@ -496,6 +497,17 @@ describe('unicornBidAdapterTest', () => { }); describe('buildBidRequest', () => { + const removeUntestableAttrs = data => { + delete data['device']; + delete data['site']['domain']; + delete data['site']['page']; + delete data['id']; + data['imp'].forEach(imp => { + delete imp['id']; + }) + delete data['user']['id']; + return data; + }; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { unicorn: { @@ -508,17 +520,6 @@ describe('unicornBidAdapterTest', () => { }); it('buildBidRequest', () => { const req = spec.buildRequests(validBidRequests, bidderRequest); - const removeUntestableAttrs = data => { - delete data['device']; - delete data['site']['domain']; - delete data['site']['page']; - delete data['id']; - data['imp'].forEach(imp => { - delete imp['id']; - }) - delete data['user']['id']; - return data; - }; const uid = JSON.parse(req.data)['user']['id']; const reqData = removeUntestableAttrs(JSON.parse(req.data)); const openRTBRequestData = removeUntestableAttrs(openRTBRequest); @@ -527,6 +528,28 @@ describe('unicornBidAdapterTest', () => { const uid2 = JSON.parse(req2.data)['user']['id']; assert.deepStrictEqual(uid, uid2); }); + it('test if contains ID5', () => { + let _validBidRequests = utils.deepClone(validBidRequests); + _validBidRequests[0].userId = { + id5id: { + uid: 'id5_XXXXX' + } + } + const req = spec.buildRequests(_validBidRequests, bidderRequest); + const reqData = removeUntestableAttrs(JSON.parse(req.data)); + const openRTBRequestData = removeUntestableAttrs(utils.deepClone(openRTBRequest)); + openRTBRequestData.user.eids = [ + { + source: 'id5-sync.com', + uids: [ + { + id: 'id5_XXXXX' + } + ] + } + ] + assert.deepStrictEqual(reqData, openRTBRequestData); + }) }); describe('interpretResponse', () => { diff --git a/test/spec/modules/unrulyBidAdapter_spec.js b/test/spec/modules/unrulyBidAdapter_spec.js index 6d1d8f9949f..abf1a54787d 100644 --- a/test/spec/modules/unrulyBidAdapter_spec.js +++ b/test/spec/modules/unrulyBidAdapter_spec.js @@ -42,9 +42,40 @@ describe('UnrulyAdapter', function () { } } - const createExchangeResponse = (...bids) => ({ - body: {bids} - }); + function createOutStreamExchangeAuctionConfig() { + return { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }; + + function createExchangeResponse (bidList, auctionConfigs = null) { + let bids = []; + if (Array.isArray(bidList)) { + bids = bidList; + } else if (bidList) { + bids.push(bidList); + } + + if (!auctionConfigs) { + return { + 'body': {bids} + }; + } + + return { + 'body': { + bids, + auctionConfigs + } + } + }; const inStreamServerResponse = { 'requestId': '262594d5d1f8104', @@ -486,7 +517,8 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b' } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; @@ -560,7 +592,8 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b', } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; @@ -651,13 +684,235 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b', } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); expect(result[0].data).to.deep.equal(expectedResult); }); + describe('Protected Audience Support', function() { + it('should return an array with 2 items and enabled protected audience', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': true, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + }, + { + 'bidder': 'unruly', + 'params': { + 'siteId': 2234554, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(2); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[1].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + expect(result[1].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + }); + it('should return an array with 2 items and enabled protected audience on only one unit', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': true, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + }, + { + 'bidder': 'unruly', + 'params': { + 'siteId': 2234554, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': {} + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(2); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[1].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + expect(result[1].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.be.undefined; + }); + it('disables configured protected audience when fledge is not availble', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': false, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(1); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.be.undefined; + }); + }); }); describe('interpretResponse', function () { @@ -705,7 +960,167 @@ describe('UnrulyAdapter', function () { renderer: fakeRenderer, mediaType: 'video' } - ]) + ]); + }); + + it('should return object with an array of bids and an array of auction configs when it receives a successful response from server', function () { + let bidId = '27a3ee1626a5c7' + const mockExchangeBid = createOutStreamExchangeBid({adUnitCode: 'video1', requestId: 'mockBidId'}); + const mockExchangeAuctionConfig = {}; + mockExchangeAuctionConfig[bidId] = createOutStreamExchangeAuctionConfig(); + const mockServerResponse = createExchangeResponse(mockExchangeBid, mockExchangeAuctionConfig); + const originalRequest = { + 'data': { + 'bidderRequest': { + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 480 + ], + [ + 300, + 250 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'bidId': bidId, + 'bidderRequestId': '12e00d17dff07b', + } + ] + } + } + }; + + expect(adapter.interpretResponse(mockServerResponse, originalRequest)).to.deep.equal({ + 'bids': [ + { + 'ext': { + 'statusCode': 1, + 'renderer': { + 'id': 'unruly_inarticle', + 'config': { + 'siteId': 123456, + 'targetingUUID': 'xxx-yyy-zzz' + }, + 'url': 'https://video.unrulymedia.com/native/prebid-loader.js' + }, + 'adUnitCode': 'video1' + }, + requestId: 'mockBidId', + bidderCode: 'unruly', + cpm: 20, + width: 323, + height: 323, + vastUrl: 'https://targeting.unrulymedia.com/in_article?uuid=74544e00-d43b-4f3a-a799-69d22ce979ce&supported_mime_type=application/javascript&supported_mime_type=video/mp4&tj=%7B%22site%22%3A%7B%22lang%22%3A%22en-GB%22%2C%22ref%22%3A%22%22%2C%22page%22%3A%22https%3A%2F%2Fdemo.unrulymedia.com%2FinArticle%2Finarticle_nypost_upbeat%2Ftravel_magazines.html%22%2C%22domain%22%3A%22demo.unrulymedia.com%22%7D%2C%22user%22%3A%7B%22profile%22%3A%7B%22quantcast%22%3A%7B%22segments%22%3A%5B%7B%22id%22%3A%22D%22%7D%2C%7B%22id%22%3A%22T%22%7D%5D%7D%7D%7D%7D&video_width=618&video_height=347', + netRevenue: true, + creativeId: 'mockBidId', + ttl: 360, + 'meta': { + 'mediaType': 'video', + 'videoContext': 'outstream' + }, + currency: 'USD', + renderer: fakeRenderer, + mediaType: 'video' + } + ], + 'fledgeAuctionConfigs': [{ + 'bidId': bidId, + 'config': { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }] + }); + }); + + it('should return object with an array of auction configs when it receives a successful response from server without bids', function () { + let bidId = '27a3ee1626a5c7'; + const mockExchangeAuctionConfig = {}; + mockExchangeAuctionConfig[bidId] = createOutStreamExchangeAuctionConfig(); + const mockServerResponse = createExchangeResponse(null, mockExchangeAuctionConfig); + const originalRequest = { + 'data': { + 'bidderRequest': { + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 480 + ], + [ + 300, + 250 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'bidId': bidId, + 'bidderRequestId': '12e00d17dff07b' + } + ] + } + } + }; + + expect(adapter.interpretResponse(mockServerResponse, originalRequest)).to.deep.equal({ + 'bids': [], + 'fledgeAuctionConfigs': [{ + 'bidId': bidId, + 'config': { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }] + }); }); it('should initialize and set the renderer', function () { @@ -875,7 +1290,7 @@ describe('UnrulyAdapter', function () { it('should return correct response for multiple bids', function () { const outStreamServerResponse = createOutStreamExchangeBid({adUnitCode: 'video1', requestId: 'mockBidId'}); - const mockServerResponse = createExchangeResponse(outStreamServerResponse, inStreamServerResponse, bannerServerResponse); + const mockServerResponse = createExchangeResponse([outStreamServerResponse, inStreamServerResponse, bannerServerResponse]); const expectedOutStreamResponse = outStreamServerResponse; expectedOutStreamResponse.mediaType = 'video'; @@ -890,7 +1305,7 @@ describe('UnrulyAdapter', function () { it('should return only valid bids', function () { const {ad, ...bannerServerResponseNoAd} = bannerServerResponse; - const mockServerResponse = createExchangeResponse(bannerServerResponseNoAd, inStreamServerResponse); + const mockServerResponse = createExchangeResponse([bannerServerResponseNoAd, inStreamServerResponse]); const expectedInStreamResponse = inStreamServerResponse; expectedInStreamResponse.mediaType = 'video'; diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 6a2d259fdd7..18f49f4943e 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -382,6 +382,93 @@ describe('User ID', function () { }); }); + describe('createEidsArray', () => { + beforeEach(() => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1', null, null, + {'mockId1': {source: 'mock1source', atype: 1}}), + createMockIdSubmodule('mockId2v1', null, null, + {'mockId2v1': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 1})}}), + createMockIdSubmodule('mockId2v2', null, null, + {'mockId2v2': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 2})}}), + ]); + }); + + it('should group UIDs by source and ext', () => { + const eids = createEidsArray({ + mockId1: ['mock-1-1', 'mock-1-2'], + mockId2v1: ['mock-2-1', 'mock-2-2'], + mockId2v2: ['mock-2-1', 'mock-2-2'] + }); + expect(eids).to.eql([ + { + source: 'mock1source', + uids: [ + { + id: 'mock-1-1', + atype: 1, + }, + { + id: 'mock-1-2', + atype: 1, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 1 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 2 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + } + ]) + }); + + it('when merging with pubCommonId, should not alter its eids', () => { + const uid = { + pubProvidedId: [ + { + source: 'mock1Source', + uids: [ + {id: 'uid2'} + ] + } + ], + mockId1: 'uid1', + }; + const eids = createEidsArray(uid); + expect(eids).to.have.length(1); + expect(eids[0].uids.map(u => u.id)).to.have.members(['uid1', 'uid2']); + expect(uid.pubProvidedId[0].uids).to.eql([{id: 'uid2'}]); + }); + }) + it('pbjs.getUserIds', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); diff --git a/test/spec/modules/viantOrtbBidAdapter_spec.js b/test/spec/modules/viantOrtbBidAdapter_spec.js index ef537d50986..73fdb7f3dc8 100644 --- a/test/spec/modules/viantOrtbBidAdapter_spec.js +++ b/test/spec/modules/viantOrtbBidAdapter_spec.js @@ -109,6 +109,49 @@ describe('viantOrtbBidAdapter', function () { }); }); }); + + describe('native', function () { + describe('and request config uses mediaTypes', () => { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 30 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let nativeBidWithMediaTypes = Object.assign({}, makeBid()); + nativeBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(nativeBidWithMediaTypes)).to.equal(false); + }); + }); + }); }); describe('buildRequests-banner', function () { @@ -172,7 +215,7 @@ describe('viantOrtbBidAdapter', function () { }); it('sends bid requests to the correct endpoint', function () { const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].url; - expect(url).to.equal('https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder_test'); + expect(url).to.equal('https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder'); }); it('sends site', function () { diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 139349ceead..5528705efd7 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -1257,6 +1257,7 @@ describe('VisxAdapter', function () { const request = spec.buildRequests(bidRequests); const pendingUrl = 'https://t.visx.net/track/pending/123123123'; const winUrl = 'https://t.visx.net/track/win/53245341'; + const runtimeUrl = 'https://t.visx.net/track/status/12345678'; const expectedResponse = [ { 'requestId': '300bfeb0d71a5b', @@ -1281,7 +1282,8 @@ describe('VisxAdapter', function () { 'ext': { 'events': { 'pending': pendingUrl, - 'win': winUrl + 'win': winUrl, + 'runtime': runtimeUrl }, 'targeting': { 'hb_visx_product': 'understitial', @@ -1298,6 +1300,9 @@ describe('VisxAdapter', function () { pending: pendingUrl, win: winUrl, }); + utils.deepSetValue(serverResponse.bid[0], 'ext.visx.events', { + runtime: runtimeUrl + }); const result = spec.interpretResponse({'body': {'seatbid': [serverResponse]}}, request); expect(result).to.deep.equal(expectedResponse); }); @@ -1325,6 +1330,39 @@ describe('VisxAdapter', function () { expect(utils.triggerPixel.calledOnceWith(trackUrl)).to.equal(true); }); + it('onBidWon with runtime tracker (0 < timeToRespond <= 5000 )', function () { + const trackUrl = 'https://t.visx.net/track/win/123123123'; + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '1', ext: { events: { win: trackUrl, runtime: runtimeUrl } }, timeToRespond: 100 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledTwice).to.equal(true); + expect(utils.triggerPixel.calledWith(trackUrl)).to.equal(true); + expect(utils.triggerPixel.calledWith(runtimeUrl.replace('{STATUS_CODE}', 999002))).to.equal(true); + }); + + it('onBidWon with runtime tracker (timeToRespond <= 0 )', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '2', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 0 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999000))).to.equal(true); + }); + + it('onBidWon with runtime tracker (timeToRespond > 5000 )', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '3', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 5001 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999100))).to.equal(true); + }); + + it('onBidWon runtime tracker should be called once per auction', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid1 = { auctionId: '4', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 100 }; + spec.onBidWon(bid1); + const bid2 = { auctionId: '4', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 200 }; + spec.onBidWon(bid2); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999002))).to.equal(true); + }); + it('onTimeout', function () { const data = [{ timeout: 3000, adUnitCode: 'adunit-code-1', auctionId: '1cbd2feafe5e8b', bidder: 'visx', bidId: '23423', params: [{ uid: '1' }] }]; const expectedData = [{ timeout: 3000, params: [{ uid: 1 }] }]; diff --git a/test/spec/modules/vrtcalBidAdapter_spec.js b/test/spec/modules/vrtcalBidAdapter_spec.js index cc4dc0a3882..938934170e9 100644 --- a/test/spec/modules/vrtcalBidAdapter_spec.js +++ b/test/spec/modules/vrtcalBidAdapter_spec.js @@ -134,4 +134,39 @@ describe('vrtcalBidAdapter', function () { ).to.be.true }) }) + + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://usync.vrtcal.com/i?ssp=1804&synctype=iframe'; + const syncurl_redirect = 'https://usync.vrtcal.com/i?ssp=1804&synctype=redirect'; + + it('base iframe sync pper config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('base redirect sync per config', function() { + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: syncurl_redirect + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with ccpa data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, 'ccpa_consent_string', undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=ccpa_consent_string&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gdpr data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: 1, consentString: 'gdpr_consent_string'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=1&gdpr_consent=gdpr_consent_string&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gpp data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined, {gppString: 'gpp_consent_string', applicableSections: [1, 5]})).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=gpp_consent_string&gpp_sid=1,5&surl=' + }]); + }); + }) }) diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index f14e8df6c09..c5f088a2306 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -1,6 +1,6 @@ import { assert, expect } from 'chai'; import { spec, NATIVE_ASSETS } from 'modules/yandexBidAdapter.js'; -import { parseUrl } from 'src/utils.js'; +import * as utils from 'src/utils.js'; import { BANNER, NATIVE } from '../../../src/mediaTypes'; import { config } from '../../../src/config'; @@ -71,7 +71,7 @@ describe('Yandex adapter', function () { expect(method).to.equal('POST'); - const parsedRequestUrl = parseUrl(url); + const parsedRequestUrl = utils.parseUrl(url); const { search: query } = parsedRequestUrl expect(parsedRequestUrl.hostname).to.equal('bs.yandex.ru'); @@ -100,25 +100,43 @@ describe('Yandex adapter', function () { const bannerRequest = getBidRequest(); const requests = spec.buildRequests([bannerRequest], bidderRequest); const { url } = requests[0]; - const parsedRequestUrl = parseUrl(url); + const parsedRequestUrl = utils.parseUrl(url); const { search: query } = parsedRequestUrl expect(query['ssp-cur']).to.equal('USD'); }); - it('should send eids if defined', function() { - const bannerRequest = getBidRequest({ + it('should send eids and ortb2 user data if defined', function() { + const bidRequestExtra = { userIdAsEids: [{ source: 'sharedid.org', - uids: [ - { - id: '01', - atype: 1 - } - ] - }] - }); + uids: [{ id: '01', atype: 1 }], + }], + ortb2: { + user: { + data: [ + { + ext: { segtax: 600, segclass: '1' }, + name: 'example.com', + segment: [{ id: '243' }], + }, + { + ext: { segtax: 600, segclass: '1' }, + name: 'ads.example.org', + segment: [{ id: '243' }], + }, + ], + }, + }, + }; + const expected = { + ext: { + eids: bidRequestExtra.userIdAsEids, + }, + data: bidRequestExtra.ortb2.user.data, + }; + const bannerRequest = getBidRequest(bidRequestExtra); const requests = spec.buildRequests([bannerRequest], bidderRequest); expect(requests).to.have.lengthOf(1); @@ -128,17 +146,7 @@ describe('Yandex adapter', function () { const { data } = request; expect(data.user).to.exist; - expect(data.user).to.deep.equal({ - ext: { - eids: [{ - source: 'sharedid.org', - uids: [{ - id: '01', - atype: 1, - }], - }], - } - }); + expect(data.user).to.deep.equal(expected); }); describe('banner', () => { @@ -478,6 +486,55 @@ describe('Yandex adapter', function () { }); }); }); + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if bid does not contain nurl', function() { + const result = spec.onBidWon({}); + expect(utils.triggerPixel.callCount).to.equal(0) + }) + + it('Should trigger pixel if bid has nurl', function() { + const result = 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({ + 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({ + 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({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=${RTT}', + }); + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=-1') + }) + }) }); function getBidConfig() { diff --git a/test/spec/modules/yieldloveBidAdapter_spec.js b/test/spec/modules/yieldloveBidAdapter_spec.js new file mode 100644 index 00000000000..b142eef0ffa --- /dev/null +++ b/test/spec/modules/yieldloveBidAdapter_spec.js @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { spec } from 'modules/yieldloveBidAdapter.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +// test params +const pid = 34437; +const rid = 'website.com'; + +describe('Yieldlove Bid Adaper', function () { + const bidRequests = [ + { + 'bidder': 'yieldlove', + 'adUnitCode': 'adunit-code', + 'sizes': [ [300, 250] ], + 'params': { + pid, + rid + } + } + ]; + + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: 'aaaa', + price: 0.5, + w: 300, + h: 250, + adm: '
test
', + crid: '1234', + } + ] + } + ], + ext: {} + } + } + + describe('isBidRequestValid', () => { + const bid = bidRequests[0]; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not present', function () { + const invalidBid = { ...bid, params: {} }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "pid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, pid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "rid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, rid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build the request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = request.data; + const url = request.url; + + expect(url).to.equal(ENDPOINT_URL); + + expect(payload.site).to.exist; + expect(payload.site.publisher).to.exist; + expect(payload.site.publisher.id).to.exist; + expect(payload.site.publisher.id).to.equal(rid); + expect(payload.site.domain).to.exist; + expect(payload.site.domain).to.equal(rid); + + expect(payload.imp).to.exist; + expect(payload.imp[0]).to.exist; + expect(payload.imp[0].ext).to.exist; + expect(payload.imp[0].ext.prebid).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(pid.toString()); + }); + }); + + describe('interpretResponse', () => { + it('should interpret the response by pushing it in the bids elem', function () { + const allResponses = spec.interpretResponse(serverResponse); + const response = allResponses[0]; + const seatbid = serverResponse.body.seatbid[0].bid[0]; + + expect(response.requestId).to.exist; + expect(response.requestId).to.equal(seatbid.impid); + expect(response.cpm).to.exist; + expect(response.cpm).to.equal(seatbid.price); + expect(response.width).to.exist; + expect(response.width).to.equal(seatbid.w); + expect(response.height).to.exist; + expect(response.height).to.equal(seatbid.h); + expect(response.ad).to.exist; + expect(response.ad).to.equal(seatbid.adm); + expect(response.ttl).to.exist; + expect(response.creativeId).to.exist; + expect(response.creativeId).to.equal(seatbid.crid); + expect(response.netRevenue).to.exist; + expect(response.currency).to.exist; + }); + }); + + describe('getUserSyncs', function() { + it('should retrieve user iframe syncs', function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], undefined, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=NaN&gdpr_consent=&' + }]); + + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], { gdprApplies: true, consentString: 'example' }, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=1&gdpr_consent=example&' + }]); + }); + }); +}) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 25fe176553d..edb3ef3af27 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -47,7 +47,7 @@ describe('YieldmoAdapter', function () { video: { playerSize: [640, 480], context: 'instream', - mimes: ['video/mp4'] + mimes: ['video/mp4'], }, }, params: { @@ -61,11 +61,11 @@ describe('YieldmoAdapter', function () { api: [2, 3], skipppable: true, playbackmethod: [1, 2], - ...videoParams - } + ...videoParams, + }, }, transactionId: '54a58774-7a41-494e-8cbc-fa7b79164f0c', - ...rootParams + ...rootParams, }); const mockBidderRequest = (params = {}, bids = [mockBannerBid()]) => ({ @@ -74,7 +74,6 @@ describe('YieldmoAdapter', function () { bidderRequestId: '14c4ede8c693f', bids, auctionStart: 1520001292880, - timeout: 3000, start: 1520001292884, doneCbCallCount: 0, refererInfo: { @@ -169,6 +168,14 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); }); + it('should pass default timeout in bid request', function () { + const requests = build([mockBannerBid()]); + expect(requests[0].data.tmax).to.equal(400); + }); + it('should pass tmax to bid request', function () { + const requests = build([mockBannerBid()], mockBidderRequest({timeout: 1000})); + expect(requests[0].data.tmax).to.equal(1000); + }); it('should not blow up if crumbs is undefined', function () { expect(function () { build([mockBannerBid({crumbs: undefined})]); @@ -425,6 +432,18 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); }); + it('should not require params.video if required props in mediaTypes.video', function () { + videoBid.mediaTypes.video = { + ...videoBid.mediaTypes.video, + ...videoBid.params.video + }; + delete videoBid.params.video; + const requests = build([videoBid]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); + it('should add mediaTypes.video prop to the imp.video prop', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['minduration'] = 40; expect(buildVideoBidAndGetVideoParam().minduration).to.equal(40); @@ -441,6 +460,16 @@ describe('YieldmoAdapter', function () { expect(buildVideoBidAndGetVideoParam().minduration).to.deep.equal(['video/mp4']); }); + it('should add plcmt value to the imp.video', function () { + const videoBid = mockVideoBid({}, {}, { plcmt: 1 }); + expect(utils.deepAccess(videoBid, 'params.video')['plcmt']).to.equal(1); + }); + + it('should add start delay if plcmt value is not 1', function () { + const videoBid = mockVideoBid({}, {}, { plcmt: 2 }); + expect(build([videoBid])[0].data.imp[0].video.startdelay).to.equal(0); + }); + it('should override mediaTypes.video.mimes prop if params.video.mimes is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['mimes'] = ['video/mp4']; utils.deepAccess(videoBid, 'params.video')['mimes'] = ['video/mkv']; diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index cbba815cfc1..5194a6a526a 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -2,19 +2,20 @@ import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; import {config} from 'src/config'; import CONSTANTS from 'src/constants.json'; import {server} from '../../mocks/xhr.js'; +import {logError} from '../../../src/utils'; let utils = require('src/utils'); let events = require('src/events'); -const MOCK = { - STUB: { - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' - }, +const EVENTS = { AUCTION_END: { 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', 'timestamp': 1638441234544, 'auctionEnd': 1638441234784, 'auctionStatus': 'completed', + 'metrics': { + 'someMetric': 1 + }, 'adUnits': [ { 'code': '/19968336/header-bid-tag-0', @@ -74,13 +75,6 @@ const MOCK = { 'bids': [ { 'bidder': 'zeta_global_ssp', - 'params': { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } - }, 'mediaTypes': { 'banner': { 'sizes': [ @@ -309,6 +303,9 @@ const MOCK = { 'cpm': 2.258302852806723, 'currency': 'USD', 'ad': 'test_ad', + 'metrics': { + 'someMetric': 0 + }, 'ttl': 200, 'creativeId': '456456456', 'netRevenue': true, @@ -344,11 +341,7 @@ const MOCK = { 'status': 'rendered', 'params': [ { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } + 'nonZetaParam': 'nonZetaValue' } ] }, @@ -392,33 +385,45 @@ describe('Zeta Global SSP Analytics Adapter', function() { zetaAnalyticsAdapter.disableAnalytics(); }); - it('events are sent', function() { - this.timeout(5000); - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_END, MOCK.AUCTION_END); - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.NO_BID, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_WON, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_DONE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.SET_TARGETING, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.ADD_AD_UNITS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_FAILED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED); - events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.STALE_RENDER, MOCK.STUB); + it('Move ZetaParams through analytics events', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); + + expect(requests.length).to.equal(2); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionSucceeded.bid.params[0]).to.be.deep.equal(EVENTS.AUCTION_END.adUnits[0].bids[0].params); + expect(EVENTS.AUCTION_END.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + }); + + it('Keep only needed fields', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); expect(requests.length).to.equal(2); - expect(JSON.parse(requests[0].requestBody)).to.deep.equal(MOCK.AUCTION_END); - expect(JSON.parse(requests[1].requestBody)).to.deep.equal(MOCK.AD_RENDER_SUCCEEDED); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionEnd.adUnitCodes[0]).to.be.equal('/19968336/header-bid-tag-0'); + 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.bidderRequests[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.bidsReceived[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.noBids[0].bidder).to.be.equal('appnexus'); + + expect(auctionSucceeded.adId).to.be.equal('5759bb3ef7be1e8'); + expect(auctionSucceeded.bid.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionSucceeded.bid.requestId).to.be.equal('206be9a13236af'); + expect(auctionSucceeded.bid.bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionSucceeded.bid.creativeId).to.be.equal('456456456'); + expect(auctionSucceeded.bid.size).to.be.equal('480x320'); + expect(auctionSucceeded.doc.location.hostname).to.be.equal('localhost'); }); }); }); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 601f4546a29..9ef97019a98 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -1,5 +1,6 @@ import {spec} from '../../../modules/zeta_global_sspBidAdapter.js' import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {deepClone} from '../../../src/utils'; describe('Zeta Ssp Bid Adapter', function () { const eids = [ @@ -50,7 +51,6 @@ describe('Zeta Ssp Bid Adapter', function () { someTag: 444, }, sid: 'publisherId', - shortname: 'test_shortname', tagid: 'test_tag_id', site: { page: 'testPage' @@ -124,7 +124,24 @@ describe('Zeta Ssp Bid Adapter', function () { uspConsent: 'someCCPAString', params: params, userIdAsEids: eids, - timeout: 500 + timeout: 500, + ortb2: { + user: { + data: [ + { + ext: { + segtax: 600, + segclass: 'classifier_v1' + }, + segment: [ + { id: '3' }, + { id: '44' }, + { id: '59' } + ] + } + ] + } + } }]; const bannerWithFewSizesRequest = [{ @@ -253,11 +270,13 @@ describe('Zeta Ssp Bid Adapter', function () { }; it('Test the bid validation function', function () { - const validBid = spec.isBidRequestValid(bannerRequest[0]); - const invalidBid = spec.isBidRequestValid(null); + const invalidBid = deepClone(bannerRequest[0]); + invalidBid.params = {}; + const isValidBid = spec.isBidRequestValid(bannerRequest[0]); + const isInvalidBid = spec.isBidRequestValid(null); - expect(validBid).to.be.true; - expect(invalidBid).to.be.false; + expect(isValidBid).to.be.true; + expect(isInvalidBid).to.be.false; }); it('Test provide eids', function () { @@ -453,7 +472,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in banner request', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -462,7 +481,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in video request', function () { const request = spec.buildRequests(videoRequest, videoRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -471,7 +490,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test multi imp', function () { const request = spec.buildRequests(multiImpRequest, multiImpRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.imp.length).to.eql(2); @@ -604,4 +623,13 @@ describe('Zeta Ssp Bid Adapter', function () { expect(bidResponse[0].ad).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); expect(bidResponse[0].vastXml).to.be.undefined; }); + + it('Test provide segments into the request', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.user.data[0].segment.length).to.eql(3); + expect(payload.user.data[0].segment[0].id).to.eql('3'); + expect(payload.user.data[0].segment[1].id).to.eql('44'); + expect(payload.user.data[0].segment[2].id).to.eql('59'); + }); }); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index cff26df2e4d..98d841d9c7c 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -965,13 +965,23 @@ describe('adapterManager tests', function () { }]; it('invokes callBids on the S2S adapter', function () { + const done = sinon.stub(); + const onTimelyResponse = sinon.stub(); + prebidServerAdapterMock.callBids.callsFake((_1, _2, _3, done) => { + done(); + }); 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)); }); // Enable this test when prebidServer adapter is made 1.0 compliant diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js index df0ce02c15c..a3a0459b980 100644 --- a/test/spec/unit/core/ajax_spec.js +++ b/test/spec/unit/core/ajax_spec.js @@ -1,7 +1,8 @@ -import {dep, attachCallbacks, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {attachCallbacks, dep, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; import {config} from 'src/config.js'; import {server} from '../../../mocks/xhr.js'; -import {sandbox} from 'sinon'; +import * as utils from 'src/utils.js'; +import {logError} from 'src/utils.js'; const EXAMPLE_URL = 'https://www.example.com'; @@ -312,13 +313,24 @@ describe('attachCallbacks', () => { const cbType = success ? 'success' : 'error'; describe(`for ${t}`, () => { - let response, body; + let sandbox, response, body; beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logError'); ({response, body} = makeResponse()); }); + afterEach(() => { + sandbox.restore(); + }) + function checkXHR(xhr) { - sinon.assert.match(xhr, { + utils.logError.resetHistory(); + const serialized = JSON.parse(JSON.stringify(xhr)) + // serialization of `responseXML` should not generate console messages + sinon.assert.notCalled(utils.logError); + + sinon.assert.match(serialized, { readyState: XMLHttpRequest.DONE, status: response.status, statusText: response.statusText, @@ -330,7 +342,7 @@ describe('attachCallbacks', () => { if (xml) { expect(xhr.responseXML.querySelectorAll('*').length > 0).to.be.true; } else { - expect(xhr.responseXML).to.not.exist; + expect(serialized.responseXML).to.not.exist; } Array.from(response.headers.entries()).forEach(([name, value]) => { expect(xhr.getResponseHeader(name)).to.eql(value); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 0360679ca52..9dcdb627698 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -48,1040 +48,1151 @@ before(() => { let wrappedCallback = config.callbackWithBidder(CODE); -describe('bidders created by newBidder', function () { - let spec; - let bidder; - let addBidResponseStub; - let doneStub; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - getUserSyncs: sinon.stub() - }; - - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - }); - - describe('when the ajax response is irrelevant', function () { - let sandbox; - let ajaxStub; - let getConfigSpy; - let aliasRegistryStub, aliasRegistry; +describe('bidderFactory', () => { + describe('bidders created by newBidder', function () { + let spec; + let bidder; + let addBidResponseStub; + let doneStub; beforeEach(function () { - sandbox = sinon.sandbox.create(); - sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); - ajaxStub = sandbox.stub(ajax, 'ajax'); - addBidResponseStub.reset(); - getConfigSpy = sandbox.spy(config, 'getConfig'); - doneStub.reset(); - aliasRegistry = {}; - aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + getUserSyncs: sinon.stub() + }; - afterEach(function () { - sandbox.restore(); + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); }); - it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } + describe('when the ajax response is irrelevant', function () { + let sandbox; + let ajaxStub; + let getConfigSpy; + let aliasRegistryStub, aliasRegistry; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); + ajaxStub = sandbox.stub(ajax, 'ajax'); + addBidResponseStub.reset(); + getConfigSpy = sandbox.spy(config, 'getConfig'); + doneStub.reset(); + aliasRegistry = {}; + aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should let registerSyncs run with valid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } + afterEach(function () { + sandbox.restore(); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + it('should let registerSyncs run with valid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - aliasRegistry = {[spec.code]: CODE}; - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); - }); - describe('transaction IDs', () => { - beforeEach(() => { - activityRules.isActivityAllowed.reset(); - ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); - spec.interpretResponse.callsFake(() => [ - { - requestId: 'bid', - cpm: 123, - ttl: 300, - creativeId: 'crid', - netRevenue: true, - currency: 'USD' + it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false } - ]) + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + aliasRegistry = {[spec.code]: CODE}; + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); }); - Object.entries({ - 'be hidden': false, - 'not be hidden': true, - }).forEach(([t, allowed]) => { - const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; + describe('transaction IDs', () => { + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); + spec.interpretResponse.callsFake(() => [ + { + requestId: 'bid', + cpm: 123, + ttl: 300, + creativeId: 'crid', + netRevenue: true, + currency: 'USD' + } + ]) + }); - function checkBidRequest(br) { - ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); - } + Object.entries({ + 'be hidden': false, + 'not be hidden': true, + }).forEach(([t, allowed]) => { + const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; - function checkBidderRequest(br) { - expectation(br.auctionId); - br.bids.forEach(checkBidRequest); - } + function checkBidRequest(br) { + ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); + } - it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { - spec.isBidRequestValid.callsFake(br => { - checkBidRequest(br); - return true; - }); - spec.buildRequests.callsFake((bidReqs, bidderReq) => { - checkBidderRequest(bidderReq); - bidReqs.forEach(checkBidRequest); - return {method: 'POST'}; - }); - activityRules.isActivityAllowed.callsFake(() => allowed); + function checkBidderRequest(br) { + expectation(br.auctionId); + br.bids.forEach(checkBidRequest); + } - const bidder = newBidder(spec); + it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { + spec.isBidRequestValid.callsFake(br => { + checkBidRequest(br); + return true; + }); + spec.buildRequests.callsFake((bidReqs, bidderReq) => { + checkBidderRequest(bidderReq); + bidReqs.forEach(checkBidRequest); + return {method: 'POST'}; + }); + activityRules.isActivityAllowed.callsFake(() => allowed); + + const bidder = newBidder(spec); + + bidder.callBids({ + bidderCode: 'mockBidder', + auctionId: 'aid', + bids: [ + { + adUnitCode: 'mockAU', + bidId: 'bid', + transactionId: 'tid', + auctionId: 'aid' + } + ] + }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { + componentType: MODULE_TYPE_BIDDER, + componentName: 'mockBidder' + }); + sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { + transactionId: 'tid', + auctionId: 'aid' + }) + }); + }); - bidder.callBids({ + it('should not be hidden from request methods', (done) => { + const bidderRequest = { bidderCode: 'mockBidder', auctionId: 'aid', + getAID() { return this.auctionId }, bids: [ { adUnitCode: 'mockAU', bidId: 'bid', transactionId: 'tid', - auctionId: 'aid' + auctionId: 'aid', + getTIDs() { + return [this.auctionId, this.transactionId] + } } ] - }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { - componentType: MODULE_TYPE_BIDDER, - componentName: 'mockBidder' + }; + activityRules.isActivityAllowed.callsFake(() => false); + spec.isBidRequestValid.returns(true); + spec.buildRequests.callsFake((reqs, bidderReq) => { + expect(bidderReq.getAID()).to.eql('aid'); + expect(reqs[0].getTIDs()).to.eql(['aid', 'tid']); + done(); }); - sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { - transactionId: 'tid', - auctionId: 'aid' - }) - }); + newBidder(spec).callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + }) }); - it('should not be hidden from request methods', (done) => { - const bidderRequest = { - bidderCode: 'mockBidder', - auctionId: 'aid', - getAID() { return this.auctionId }, - bids: [ - { - adUnitCode: 'mockAU', - bidId: 'bid', - transactionId: 'tid', - auctionId: 'aid', - getTIDs() { - return [this.auctionId, this.transactionId] - } - } - ] - }; - activityRules.isActivityAllowed.callsFake(() => false); - spec.isBidRequestValid.returns(true); - spec.buildRequests.callsFake((reqs, bidderReq) => { - expect(bidderReq.getAID()).to.eql('aid'); - expect(reqs[0].getTIDs()).to.eql(['aid', 'tid']); - done(); - }); - newBidder(spec).callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - }) - }); - - it('should handle bad bid requests gracefully', function () { - const bidder = newBidder(spec); - - spec.getUserSyncs.returns([]); + it('should handle bad bid requests gracefully', function () { + const bidder = newBidder(spec); - bidder.callBids({}); - bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.getUserSyncs.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.called).to.equal(false); - expect(spec.buildRequests.called).to.equal(false); - expect(spec.interpretResponse.called).to.equal(false); - }); + bidder.callBids({}); + bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should call buildRequests(bidRequest) the params are valid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.called).to.equal(false); + expect(spec.buildRequests.called).to.equal(false); + expect(spec.interpretResponse.called).to.equal(false); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should call buildRequests(bidRequest) the params are valid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not call buildRequests the params are invalid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); + }); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); + it('should not call buildRequests the params are invalid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should filter out invalid bids before calling buildRequests', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.called).to.equal(false); + }); - spec.isBidRequestValid.onFirstCall().returns(true); - spec.isBidRequestValid.onSecondCall().returns(false); - spec.buildRequests.returns([]); + it('should filter out invalid bids before calling buildRequests', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.onFirstCall().returns(true); + spec.isBidRequestValid.onSecondCall().returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make no server requests if the spec doesn\'t return any', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should make no server requests if the spec doesn\'t return any', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data + expect(ajaxStub.called).to.equal(false); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - sinon.assert.match(ajaxStub.firstCall.args[3], { - method: 'POST', - contentType: 'text/plain', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const options = { contentType: 'application/json' }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data, - options: options + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'text/plain', + withCredentials: true + }); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const options = { contentType: 'application/json' }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data, + options: options + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - sinon.assert.match(ajaxStub.firstCall.args[3], { - method: 'POST', - contentType: 'application/json', - withCredentials: true - }) - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate GET request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'application/json', + withCredentials: true + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate GET request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - sinon.assert.match(ajaxStub.firstCall.args[3], { - method: 'GET', - withCredentials: true - }) - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate GET request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const opt = { withCredentials: false } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data, - options: opt + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: true + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - sinon.assert.match(ajaxStub.firstCall.args[3], { - method: 'GET', - withCredentials: false - }) - }); - - it('should make multiple calls if the spec returns them', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { - method: 'POST', - url: url, - data: data - }, - { + it('should make the appropriate GET request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const opt = { withCredentials: false } + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'GET', url: url, - data: data - } - ]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + data: data, + options: opt + }); - expect(ajaxStub.calledTwice).to.equal(true); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - describe('browsingTopics ajax option', () => { - let transmitUfpdAllowed, bidder; - beforeEach(() => { - activityRules.isActivityAllowed.reset(); - activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); - bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: false + }) }); - it(`should be set to false when adapter sets browsingTopics = false`, () => { - transmitUfpdAllowed = true; + it('should make multiple calls if the spec returns them', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); spec.buildRequests.returns([ + { + method: 'POST', + url: url, + data: data + }, { method: 'GET', - url: 'url', - options: { - browsingTopics: false - } + url: url, + data: data } ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ - browsingTopics: false - })); + + expect(ajaxStub.calledTwice).to.equal(true); }); - Object.entries({ - 'allowed': true, - 'not allowed': false - }).forEach(([t, allow]) => { - it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { - transmitUfpdAllowed = allow; + describe('browsingTopics ajax option', () => { + let transmitUfpdAllowed, bidder, origBS; + before(() => { + origBS = window.$$PREBID_GLOBAL$$.bidderSettings; + }) + + after(() => { + window.$$PREBID_GLOBAL$$.bidderSettings = origBS; + }); + + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); + bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + }); + + it(`should be set to false when adapter sets browsingTopics = false`, () => { + transmitUfpdAllowed = true; spec.buildRequests.returns([ { method: 'GET', - url: '1', - }, - { - method: 'POST', - url: '2', - data: {} - }, - { - method: 'GET', - url: '3', + url: 'url', options: { - browsingTopics: true - } - }, - { - method: 'POST', - url: '4', - data: {}, - options: { - browsingTopics: true + browsingTopics: false } } ]); bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - ['1', '2', '3', '4'].forEach(url => { - sinon.assert.calledWith( - ajaxStub, - url, - sinon.match.any, - sinon.match.any, - sinon.match({browsingTopics: allow}) - ); - }); + sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: false + })); }); + + Object.entries({ + 'omitted': [undefined, true], + 'enabled': [true, true], + 'disabled': [false, false] + }).forEach(([t, [topicsHeader, enabled]]) => { + describe(`when bidderSettings.topicsHeader is ${t}`, () => { + beforeEach(() => { + window.$$PREBID_GLOBAL$$.bidderSettings = { + [CODE]: { + topicsHeader: topicsHeader + } + } + }); + + afterEach(() => { + delete window.$$PREBID_GLOBAL$$.bidderSettings[CODE]; + }); + + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, allow]) => { + const shouldBeSet = allow && enabled; + + it(`should be set to ${shouldBeSet} when transmitUfpd is ${t}`, () => { + transmitUfpdAllowed = allow; + spec.buildRequests.returns([ + { + method: 'GET', + url: '1', + }, + { + method: 'POST', + url: '2', + data: {} + }, + { + method: 'GET', + url: '3', + options: { + browsingTopics: true + } + }, + { + method: 'POST', + url: '4', + data: {}, + options: { + browsingTopics: true + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + ['1', '2', '3', '4'].forEach(url => { + sinon.assert.calledWith( + ajaxStub, + url, + sinon.match.any, + sinon.match.any, + sinon.match({browsingTopics: shouldBeSet}) + ); + }); + }); + }); + }) + }) }); - }); - it('should not add bids for each placement code if no requests are given', function () { - const bidder = newBidder(spec); + it('should not add bids for each placement code if no requests are given', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.callCount).to.equal(0); - }); + expect(addBidResponseStub.callCount).to.equal(0); + }); - it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { - const bidder = newBidder(spec); - const req = { - method: 'POST', - url: 'test.url.com', - data: { arg: 2 } - }; + it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { + const bidder = newBidder(spec); + const req = { + method: 'POST', + url: 'test.url.com', + data: { arg: 2 } + }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([req, req]); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([req, req]); - const eventEmitterSpy = sinon.spy(events, 'emit'); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const eventEmitterSpy = sinon.spy(events, 'emit'); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(ajaxStub.calledTwice).to.equal(true); - expect(eventEmitterSpy.getCalls() - .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) - ).to.length(2); + expect(ajaxStub.calledTwice).to.equal(true); + expect(eventEmitterSpy.getCalls() + .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) + ).to.length(2); - eventEmitterSpy.restore(); + eventEmitterSpy.restore(); + }); }); - }); - describe('when the ajax call succeeds', function () { - let ajaxStub; - let userSyncStub; - let logErrorSpy; + describe('when the ajax call succeeds', function () { + let ajaxStub; + let userSyncStub; + let logErrorSpy; - beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); + beforeEach(function () { + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); + }); + addBidResponseStub.reset(); + doneStub.resetBehavior(); + userSyncStub = sinon.stub(userSync, 'registerSync') + logErrorSpy = sinon.spy(utils, 'logError'); }); - addBidResponseStub.reset(); - doneStub.resetBehavior(); - userSyncStub = sinon.stub(userSync, 'registerSync') - logErrorSpy = sinon.spy(utils, 'logError'); - }); - afterEach(function () { - ajaxStub.restore(); - userSyncStub.restore(); - utils.logError.restore(); - }); + afterEach(function () { + ajaxStub.restore(); + userSyncStub.restore(); + utils.logError.restore(); + }); - it('should call spec.interpretResponse() with the response content', function () { - const bidder = newBidder(spec); + it('should call spec.interpretResponse() with the response content', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledOnce).to.equal(true); + const response = spec.interpretResponse.firstCall.args[0] + expect(response.body).to.equal('response body') + expect(response.headers.get('some-header')).to.equal('headerContent'); + expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + expect(doneStub.calledOnce).to.equal(true); }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.interpretResponse() once for each request made', function () { + const bidder = newBidder(spec); - expect(spec.interpretResponse.calledOnce).to.equal(true); - const response = spec.interpretResponse.firstCall.args[0] - expect(response.body).to.equal('response body') - expect(response.headers.get('some-header')).to.equal('headerContent'); - expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + ]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledTwice).to.equal(true); + expect(doneStub.calledOnce).to.equal(true); }); - expect(doneStub.calledOnce).to.equal(true); - }); - it('should call spec.interpretResponse() once for each request made', function () { - const bidder = newBidder(spec); + it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { + const bid = { + creativeId: 'creative-id', + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + adUnitCode: 'mock/placement', + currency: 'USD', + netRevenue: true, + ttl: 300, + bidderCode: 'sampleBidder', + sampleBidder: {advertiserId: '12345', networkId: '111222'} + }; + const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); + bidderRequest.bids[0].bidder = 'sampleBidder'; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - { + }); + spec.getUserSyncs.returns([]); + + spec.interpretResponse.returns(bid); + + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + let bidObject = addBidResponseStub.firstCall.args[1]; + // checking the fields added by our code + expect(bidObject.originalCpm).to.equal(bid.cpm); + expect(bidObject.originalCurrency).to.equal(bid.currency); + expect(doneStub.calledOnce).to.equal(true); + expect(logErrorSpy.callCount).to.equal(0); + expect(bidObject.meta).to.exist; + expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); + }); + + it('should call spec.getUserSyncs() with the response', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - ]); - spec.getUserSyncs.returns([]); + }); + spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(spec.interpretResponse.calledTwice).to.equal(true); - expect(doneStub.calledOnce).to.equal(true); - }); + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); + expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); + }); - it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { - const bidder = newBidder(spec); + it('should register usersync pixels', function () { + const bidder = newBidder(spec); - const bid = { - creativeId: 'creative-id', - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - adUnitCode: 'mock/placement', - currency: 'USD', - netRevenue: true, - ttl: 300, - bidderCode: 'sampleBidder', - sampleBidder: {advertiserId: '12345', networkId: '111222'} - }; - const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); - bidderRequest.bids[0].bidder = 'sampleBidder'; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); + spec.getUserSyncs.returns([{ + type: 'iframe', + url: 'usersync.com' + }]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(userSyncStub.called).to.equal(true); + expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); + expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); + expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should logError and reject bid when required bid response params are missing', function () { + const bidder = newBidder(spec); - bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const bid = { + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + placementCode: 'mock/placement' + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - let bidObject = addBidResponseStub.firstCall.args[1]; - // checking the fields added by our code - expect(bidObject.originalCpm).to.equal(bid.cpm); - expect(bidObject.originalCurrency).to.equal(bid.currency); - expect(doneStub.calledOnce).to.equal(true); - expect(logErrorSpy.callCount).to.equal(0); - expect(bidObject.meta).to.exist; - expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); - }); + spec.interpretResponse.returns(bid); - it('should call spec.getUserSyncs() with the response', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should logError and reject bid when required response params are undefined', function () { + const bidder = newBidder(spec); - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); - expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); - }); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': undefined, + 'netRevenue': true, + 'ttl': 360 + }; - it('should register usersync pixels', function () { - const bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); - spec.getUserSyncs.returns([{ - type: 'iframe', - url: 'usersync.com' - }]); + spec.interpretResponse.returns(bid); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(userSyncStub.called).to.equal(true); - expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); - expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); - expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); - }); + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - it('should logError and reject bid when required bid response params are missing', function () { - const bidder = newBidder(spec); + it('should require requestId from interpretResponse', () => { + const bidder = newBidder(spec); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + spec.interpretResponse.returns(bid); - const bid = { - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - placementCode: 'mock/placement' + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.called).to.be.false; + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); + }); + + describe('when the ajax call fails', function () { + let ajaxStub; + let callBidderErrorStub; + let eventEmitterStub; + let xhrErrorMock = { + status: 500, + statusText: 'Internal Server Error' }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + + beforeEach(function () { + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + callbacks.error('ajax call failed.', xhrErrorMock); + }); + callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); + eventEmitterStub = sinon.stub(events, 'emit'); + addBidResponseStub.reset(); + doneStub.reset(); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + afterEach(function () { + ajaxStub.restore(); + callBidderErrorStub.restore(); + eventEmitterStub.restore(); + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not spec.interpretResponse()', function () { + const bidder = newBidder(spec); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - it('should logError and reject bid when required response params are undefined', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': undefined, - 'netRevenue': true, - 'ttl': 360 - }; + expect(spec.interpretResponse.called).to.equal(false); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + it('should not add bids for each adunit code into the auction', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.callCount).to.equal(0); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should require requestId from interpretResponse', () => { - const bidder = newBidder(spec); - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - expect(addBidResponseStub.called).to.be.false; - expect(addBidResponseStub.reject.calledOnce).to.be.true; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); }); }); - describe('when the ajax call fails', function () { - let ajaxStub; - let callBidderErrorStub; - let eventEmitterStub; - let xhrErrorMock = { - status: 500, - statusText: 'Internal Server Error' - }; + describe('registerBidder', function () { + let registerBidAdapterStub; + let aliasBidAdapterStub; beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - callbacks.error('ajax call failed.', xhrErrorMock); - }); - callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); - eventEmitterStub = sinon.stub(events, 'emit'); - addBidResponseStub.reset(); - doneStub.reset(); + registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); + aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); }); afterEach(function () { - ajaxStub.restore(); - callBidderErrorStub.restore(); - eventEmitterStub.restore(); + registerBidAdapterStub.restore(); + aliasBidAdapterStub.restore(); }); - it('should not spec.interpretResponse()', function () { - const bidder = newBidder(spec); + function newEmptySpec() { + return { + code: CODE, + isBidRequestValid: function() { }, + buildRequests: function() { }, + interpretResponse: function() { }, + }; + } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.interpretResponse.called).to.equal(false); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); - }); + it('should register a bidder with the adapterManager', function () { + registerBidder(newEmptySpec()); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); + expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - it('should not add bids for each adunit code into the auction', function () { - const bidder = newBidder(spec); + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.callCount).to.equal(0); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + it('should register a bidder with the appropriate mediaTypes', function () { + const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); + registerBidder(thisSpec); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register bidders with the appropriate aliases', function () { + const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); + registerBidder(thisSpec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); - }); + expect(registerBidAdapterStub.calledThrice).to.equal(true); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + // Make sure our later calls don't override the bidder code from previous calls. + expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') }); - }); -}); -describe('registerBidder', function () { - let registerBidAdapterStub; - let aliasBidAdapterStub; + it('should register alias with their gvlid', function() { + const aliases = [ + { + code: 'foo', + gvlid: 1 + }, + { + code: 'bar', + gvlid: 2 + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); - beforeEach(function () { - registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); - aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); - }); + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); + }) - afterEach(function () { - registerBidAdapterStub.restore(); - aliasBidAdapterStub.restore(); - }); + it('should register alias with skipPbsAliasing', function() { + const aliases = [ + { + code: 'foo', + skipPbsAliasing: true + }, + { + code: 'bar', + skipPbsAliasing: false + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); - function newEmptySpec() { - return { - code: CODE, - isBidRequestValid: function() { }, - buildRequests: function() { }, - interpretResponse: function() { }, - }; - } - - it('should register a bidder with the adapterManager', function () { - registerBidder(newEmptySpec()); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); - expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; - }); + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); + }) + }) - it('should register a bidder with the appropriate mediaTypes', function () { - const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); - registerBidder(thisSpec); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); - }); + describe('validate bid response: ', function () { + let spec; + let indexStub, adUnits, bidderRequests; + let addBidResponseStub; + let doneStub; + let ajaxStub; + let logErrorSpy; + + let bids = [{ + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }]; + + beforeEach(function () { + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + }; - it('should register bidders with the appropriate aliases', function () { - const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); - registerBidder(thisSpec); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); - expect(registerBidAdapterStub.calledThrice).to.equal(true); + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); + }); + logErrorSpy = sinon.spy(utils, 'logError'); + indexStub = sinon.stub(auctionManager, 'index'); + adUnits = []; + bidderRequests = []; + indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) + }); - // Make sure our later calls don't override the bidder code from previous calls. - expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') + afterEach(function () { + ajaxStub.restore(); + logErrorSpy.restore(); + indexStub.restore; + }); - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') - }); + if (FEATURES.NATIVE) { + it('should add native bids that do have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + } + }] + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; - it('should register alias with their gvlid', function() { - const aliases = [ - { - code: 'foo', - gvlid: 1 - }, - { - code: 'bar', - gvlid: 2 - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + let bids1 = Object.assign({}, + bids[0], + { + 'mediaType': 'native', + 'native': { + 'title': 'Native Creative', + 'clickUrl': 'https://www.link.example', + } + } + ); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); - }) + const bidder = newBidder(spec); - it('should register alias with skipPbsAliasing', function() { - const aliases = [ - { - code: 'foo', - skipPbsAliasing: true - }, - { - code: 'bar', - skipPbsAliasing: false - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); - }) -}) + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); -describe('validate bid response: ', function () { - let spec; - let indexStub, adUnits, bidderRequests; - let addBidResponseStub; - let doneStub; - let ajaxStub; - let logErrorSpy; - - let bids = [{ - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }]; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - }; - - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); + it('should not add native bids that do not have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + }, + }]; + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: CODE, + mediaType: 'native', + native: { + title: undefined, + clickUrl: 'https://www.link.example', + } + } + ); - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); - }); - logErrorSpy = sinon.spy(utils, 'logError'); - indexStub = sinon.stub(auctionManager, 'index'); - adUnits = []; - bidderRequests = []; - indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - afterEach(function () { - ajaxStub.restore(); - logErrorSpy.restore(); - indexStub.restore; - }); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + }); + } - if (FEATURES.NATIVE) { - it('should add native bids that do have required assets', function () { + it('should add bid when renderer is present on outstream bids', function () { adUnits = [{ transactionId: 'au', - nativeParams: { - title: {'required': true}, + mediaTypes: { + video: {context: 'outstream'} } }] - decorateAdUnitsWithNativeParams(adUnits); let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', transactionId: 'au', + adUnitCode: 'mock/placement', params: { param: 5 }, - mediaType: 'native', }] }; let bids1 = Object.assign({}, bids[0], { - 'mediaType': 'native', - 'native': { - 'title': 'Native Creative', - 'clickUrl': 'https://www.link.example', - } + bidderCode: CODE, + mediaType: 'video', + renderer: {render: () => true, url: 'render.js'}, } ); @@ -1095,413 +1206,335 @@ describe('validate bid response: ', function () { expect(logErrorSpy.callCount).to.equal(0); }); - it('should not add native bids that do not have required assets', function () { - adUnits = [{ - transactionId: 'au', - nativeParams: { - title: {'required': true}, - }, - }]; - decorateAdUnitsWithNativeParams(adUnits); + it('should add banner bids that have no width or height but single adunit size', function () { let bidRequest = { bids: [{ + bidder: CODE, bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', - transactionId: 'au', params: { param: 5 }, - mediaType: 'native', + sizes: [[300, 250]], }] }; + bidderRequests = [bidRequest]; let bids1 = Object.assign({}, bids[0], { - bidderCode: CODE, - mediaType: 'native', - native: { - title: undefined, - clickUrl: 'https://www.link.example', - } + width: undefined, + height: undefined } ); const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); }); - } - - it('should add bid when renderer is present on outstream bids', function () { - adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }] - let bidRequest = { - bids: [{ - bidId: '1', - auctionId: 'first-bid-id', - transactionId: 'au', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - }] - }; - - let bids1 = Object.assign({}, - bids[0], - { - bidderCode: CODE, - mediaType: 'video', - renderer: {render: () => true, url: 'render.js'}, - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - it('should add banner bids that have no width or height but single adunit size', function () { - let bidRequest = { - bids: [{ - bidder: CODE, - bidId: '1', - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - sizes: [[300, 250]], - }] - }; - bidderRequests = [bidRequest]; - let bids1 = Object.assign({}, - bids[0], - { - width: undefined, - height: undefined - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - it('should disregard auctionId/transactionId set by the adapter', () => { - let bidderRequest = { - bids: [{ - bidder: CODE, - bidId: '1', - auctionId: 'aid', - transactionId: 'tid', - adUnitCode: 'au', - }] - }; - const bidder = newBidder(spec); - spec.interpretResponse.returns(Object.assign({}, bids[0], {transactionId: 'ignored', auctionId: 'ignored'})); - bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - sinon.assert.calledWith(addBidResponseStub, sinon.match.any, sinon.match({ - transactionId: 'tid', - auctionId: 'aid' - })); - }) - - describe(' Check for alternateBiddersList ', function() { - let bidRequest; - let bids1; - let logWarnSpy; - let bidderSettingStub, aliasRegistryStub; - let aliasRegistry; - beforeEach(function () { - bidRequest = { + it('should disregard auctionId/transactionId set by the adapter', () => { + let bidderRequest = { bids: [{ - bidId: '1', bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', + bidId: '1', + auctionId: 'aid', + transactionId: 'tid', + adUnitCode: 'au', }] }; + const bidder = newBidder(spec); + spec.interpretResponse.returns(Object.assign({}, bids[0], {transactionId: 'ignored', auctionId: 'ignored'})); + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(addBidResponseStub, sinon.match.any, sinon.match({ + transactionId: 'tid', + auctionId: 'aid' + })); + }) - bids1 = Object.assign({}, - bids[0], - { - bidderCode: 'validalternatebidder', - adapterCode: 'knownadapter1' - } - ); - logWarnSpy = sinon.spy(utils, 'logWarn'); - bidderSettingStub = sinon.stub(bidderSettings, 'get'); - aliasRegistry = {}; - aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + describe(' Check for alternateBiddersList ', function() { + let bidRequest; + let bids1; + let logWarnSpy; + let bidderSettingStub, aliasRegistryStub; + let aliasRegistry; - afterEach(function () { - logWarnSpy.restore(); - bidderSettingStub.restore(); - aliasRegistryStub.restore(); - }); + beforeEach(function () { + bidRequest = { + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; - it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { - bidderSettingStub.returns(false); + bids1 = Object.assign({}, + bids[0], + { + bidderCode: 'validalternatebidder', + adapterCode: 'knownadapter1' + } + ); + logWarnSpy = sinon.spy(utils, 'logWarn'); + bidderSettingStub = sinon.stub(bidderSettings, 'get'); + aliasRegistry = {}; + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + afterEach(function () { + logWarnSpy.restore(); + bidderSettingStub.restore(); + aliasRegistryStub.restore(); + }); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { + bidderSettingStub.returns(false); - it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { - bidderSettingStub.returns(undefined); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { + bidderSettingStub.returns(undefined); - it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); - it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); + + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); - expect(addBidResponseStub.called).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - aliasRegistry = {'validAlternateBidder': CODE}; + expect(addBidResponseStub.called).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + aliasRegistry = {'validAlternateBidder': CODE}; - expect(addBidResponseStub.called).to.equal(false); - expect(logWarnSpy.callCount).to.equal(1); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + expect(addBidResponseStub.called).to.equal(false); + expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); }); - }); - describe('when interpretResponse returns BidderAuctionResponse', function() { - const bidRequest = { - bids: [{ + describe('when interpretResponse returns BidderAuctionResponse', function() { + const bidRequest = { + auctionId: 'aid', + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'aid', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + const fledgeAuctionConfig = { bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - config: { - foo: 'bar' - } - } - describe('when response has FLEDGE auction config', function() { - let fledgeStub; - - function fledgeHook(next, ...args) { - fledgeStub(...args); + config: { + foo: 'bar' + } } + describe('when response has FLEDGE auction config', function() { + let fledgeStub; - before(() => { - addComponentAuction.before(fledgeHook); - }); + function fledgeHook(next, ...args) { + fledgeStub(...args); + } - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) + before(() => { + addComponentAuction.before(fledgeHook); + }); - beforeEach(function () { - fledgeStub = sinon.stub(); - }); + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) - it('should unwrap bids', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [] + beforeEach(function () { + fledgeStub = sinon.stub(); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - }); - it('should call fledgeManager with FLEDGE configs', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [fledgeAuctionConfig] + 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'); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(fledgeStub.calledOnce).to.equal(true); - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - }) + it('should call fledgeManager with FLEDGE configs', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + fledgeAuctionConfigs: [fledgeAuctionConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - 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); + 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'); + }) - expect(fledgeStub.calledOnce).to.be.true; - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(false); + 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); + + expect(fledgeStub.calledOnce).to.be.true; + sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(false); + }) }) }) - }) -}); + }); -describe('bid response isValid', () => { - describe('size check', () => { - let req, index; + describe('bid response isValid', () => { + describe('size check', () => { + let req, index; - beforeEach(() => { - req = { - ...MOCK_BIDS_REQUEST.bids[0], - mediaTypes: { - banner: { - sizes: [[1, 2], [3, 4]] + beforeEach(() => { + req = { + ...MOCK_BIDS_REQUEST.bids[0], + mediaTypes: { + banner: { + sizes: [[1, 2], [3, 4]] + } } } - } - }); + }); - function mkResponse(width, height) { - return { - requestId: req.bidId, - width, - height, - cpm: 1, - ttl: 60, - creativeId: '123', - netRevenue: true, - currency: 'USD', - mediaType: 'banner', + function mkResponse(width, height) { + return { + requestId: req.bidId, + width, + height, + cpm: 1, + ttl: 60, + creativeId: '123', + netRevenue: true, + currency: 'USD', + mediaType: 'banner', + } } - } - function checkValid(bid) { - return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); - } + function checkValid(bid) { + return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); + } - it('should succeed when response has a size that was in request', () => { - expect(checkValid(mkResponse(3, 4))).to.be.true; - }); - }) -}); + it('should succeed when response has a size that was in request', () => { + expect(checkValid(mkResponse(3, 4))).to.be.true; + }); + }) + }); +}) diff --git a/test/spec/unit/core/events_spec.js b/test/spec/unit/core/events_spec.js new file mode 100644 index 00000000000..e1451f657b5 --- /dev/null +++ b/test/spec/unit/core/events_spec.js @@ -0,0 +1,45 @@ +import {config} from 'src/config.js'; +import {emit, clearEvents, getEvents, on, off} from '../../../../src/events.js'; +import * as utils from '../../../../src/utils.js' + +describe('events', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + clearEvents(); + }); + afterEach(() => { + clock.restore(); + }); + + it('should clear event log using eventHistoryTTL config', () => { + emit('testEvent', {}); + expect(getEvents().length).to.eql(1); + config.setConfig({eventHistoryTTL: 1}); + clock.tick(500); + expect(getEvents().length).to.eql(1); + clock.tick(6000); + expect(getEvents().length).to.eql(0); + }); + + it('should take history TTL in seconds', () => { + emit('testEvent', {}); + config.setConfig({eventHistoryTTL: 1000}); + clock.tick(10000); + expect(getEvents().length).to.eql(1); + }); + + it('should include the eventString if a callback fails', () => { + const logErrorStub = sinon.stub(utils, 'logError'); + const eventString = 'bidWon'; + let fn = function() { throw new Error('Test error'); }; + on(eventString, fn); + + emit(eventString, {}); + + sinon.assert.calledWith(logErrorStub, 'Error executing handler:', 'events.js', sinon.match.instanceOf(Error), eventString); + + off(eventString, fn); + logErrorStub.restore(); + }); +}) diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index f16d6208087..ba9aeff70d1 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -1,13 +1,19 @@ -import { expect } from 'chai'; -import { targeting as targetingInstance, filters, getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm } from 'src/targeting.js'; -import { config } from 'src/config.js'; -import { createBidReceived } from 'test/fixtures/fixtures.js'; +import {expect} from 'chai'; +import { + filters, + getHighestCpmBidsFromBidPool, + sortByDealAndPriceBucketOrCpm, + targeting as targetingInstance +} from 'src/targeting.js'; +import {config} from 'src/config.js'; +import {createBidReceived} from 'test/fixtures/fixtures.js'; import CONSTANTS from 'src/constants.json'; -import { auctionManager } from 'src/auctionManager.js'; +import {auctionManager} from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; import {deepClone} from 'src/utils.js'; import {createBid} from '../../../../src/bidfactory.js'; import {hook} from '../../../../src/hook.js'; +import {getHighestCpm} from '../../../../src/utils/reducers.js'; function mkBid(bid, status = CONSTANTS.STATUS.GOOD) { return Object.assign(createBid(status), bid); @@ -451,7 +457,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -467,7 +473,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -949,6 +955,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); useBidCache = false; @@ -956,6 +963,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); }); it('should use bidCacheFilterFunction', function() { @@ -983,9 +991,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching Off, No Filter Function useBidCache = false; @@ -994,9 +1006,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On AGAIN, No Filter Function (should be same as first time) useBidCache = true; @@ -1005,9 +1021,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On, with Filter Function to Exclude video useBidCache = true; @@ -1020,9 +1040,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should have been called for each cached bid (4 times) expect(bcffCalled).to.equal(4); @@ -1038,9 +1062,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should not have been called expect(bcffCalled).to.equal(0); }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 5c361d186c0..39123d4aa41 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -14,7 +14,7 @@ import { config as configObj } from 'src/config.js'; import * as ajaxLib from 'src/ajax.js'; import * as auctionModule from 'src/auction.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; -import { _sendAdToCreative } from 'src/secureCreatives.js'; +import {resizeRemoteCreative} from 'src/secureCreatives.js'; import {find} from 'src/polyfill.js'; import * as pbjsModule from 'src/prebid.js'; import {hook} from '../../../src/hook.js'; @@ -25,7 +25,7 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {createBid} from '../../../src/bidfactory.js'; import {enrichFPD} from '../../../src/fpd/enrichment.js'; import {mockFpdEnrichments} from '../../helpers/fpd.js'; - +import {generateUUID} from '../../../src/utils.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -43,13 +43,13 @@ var adUnits = getAdUnits(); var adUnitCodes = getAdUnits().map(unit => unit.code); var bidsBackHandler = function() {}; const timeout = 2000; -var auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout}); -auction.getBidRequests = getBidRequests; -auction.getBidsReceived = getBidResponses; -auction.getAdUnits = getAdUnits; -auction.getAuctionStatus = function() { return auctionModule.AUCTION_COMPLETED } +const auctionId = generateUUID(); +let auction; function resetAuction() { + if (auction == null) { + auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout, labels: undefined, auctionId: auctionId}); + } $$PREBID_GLOBAL$$.setConfig({ enableSendAllBids: false }); auction.getBidRequests = getBidRequests; auction.getBidsReceived = getBidResponses; @@ -1104,7 +1104,7 @@ describe('Unit: Prebid Module', function () { adUnitCode: config.adUnitCodes[0], }; - _sendAdToCreative(mockAdObject, sinon.stub()); + resizeRemoteCreative(mockAdObject); expect(slots[0].spyGetSlotElementId.called).to.equal(false); expect(slots[1].spyGetSlotElementId.called).to.equal(true); @@ -1233,7 +1233,8 @@ describe('Unit: Prebid Module', function () { } }, getElementsByTagName: sinon.stub(), - querySelector: sinon.stub() + querySelector: sinon.stub(), + createElement: sinon.stub(), }; elStub = { @@ -1264,7 +1265,7 @@ describe('Unit: Prebid Module', function () { it('should require doc and id params', function () { $$PREBID_GLOBAL$$.renderAd(); - var error = 'Error trying to write ad Id :undefined to the page. Missing adId'; + var error = 'Error rendering ad (id: undefined): missing adId'; assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); }); @@ -1289,14 +1290,13 @@ describe('Unit: Prebid Module', function () { adUrl: 'http://server.example.com/ad/ad.js' }); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.ok(elStub.insertBefore.called, 'url was written to iframe in doc'); + sinon.assert.calledWith(doc.createElement, 'iframe'); }); it('should log an error when no ad or url', function () { pushBidResponseToAuction({}); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var error = 'Error trying to write ad. No ad for bid response id: ' + bidId; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + sinon.assert.called(spyLogError); }); it('should log an error when not in an iFrame', function () { @@ -1305,7 +1305,7 @@ describe('Unit: Prebid Module', function () { }); inIframe = false; $$PREBID_GLOBAL$$.renderAd(document, bidId); - const error = 'Error trying to write ad. Ad render call ad id ' + bidId + ' was prevented from writing to the main document.'; + const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; assert.ok(spyLogError.calledWith(error), 'expected error was logged'); }); @@ -1326,14 +1326,14 @@ describe('Unit: Prebid Module', function () { doc.write = sinon.stub().throws(error); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var errorMessage = 'Error trying to write ad Id :' + bidId + ' to the page:' + error.message; + var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); }); it('should log an error when ad not found', function () { var fakeId = 99; $$PREBID_GLOBAL$$.renderAd(doc, fakeId); - var error = 'Error trying to write ad. Cannot find ad by given id : ' + fakeId; + var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` assert.ok(spyLogError.calledWith(error), 'expected error was logged'); }); @@ -1345,14 +1345,6 @@ describe('Unit: Prebid Module', function () { assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); }); - it('should replace ${CLICKTHROUGH} macro in winning bids response', function () { - pushBidResponseToAuction({ - ad: "" - }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId, {clickThrough: 'https://someadserverclickurl.com'}); - expect(adResponse).to.have.property('ad').and.to.match(/https:\/\/someadserverclickurl\.com/i); - }); - it('fires billing url if present on s2s bid', function () { const burl = 'http://www.example.com/burl'; pushBidResponseToAuction({ @@ -2736,6 +2728,13 @@ describe('Unit: Prebid Module', function () { events.on.restore(); }); + it('should emit event BID_ACCEPTED when invoked', function () { + var callback = sinon.spy(); + $$PREBID_GLOBAL$$.onEvent('bidAccepted', callback); + events.emit(CONSTANTS.EVENTS.BID_ACCEPTED); + sinon.assert.calledOnce(callback); + }); + describe('beforeRequestBids', function () { let bidRequestedHandler; let beforeRequestBidsHandler; @@ -3304,16 +3303,20 @@ describe('Unit: Prebid Module', function () { const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); expect(highestBid).to.deep.equal(_bidsReceived[2]) }) - }) + }); - describe('getHighestCpm', () => { + describe('getHighestCpmBids', () => { after(() => { resetAuction(); }); it('returns an array containing the highest bid object for the given adUnitCode', function () { - const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids('/19968336/header-bid-tag-0'); + const adUnitcode = '/19968336/header-bid-tag-0'; + targeting.setLatestAuctionForAdUnit(adUnitcode, auctionId) + const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids(adUnitcode); expect(highestCpmBids.length).to.equal(1); - expect(highestCpmBids[0]).to.deep.equal(auctionManager.getBidsReceived()[1]); + const expectedBid = auctionManager.getBidsReceived()[1]; + expectedBid.latestTargetedAuctionId = auctionId; + expect(highestCpmBids[0]).to.deep.equal(expectedBid); }); it('returns an empty array when the given adUnit is not found', function () { diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 7d5f9af35dd..895bf03165a 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,6 +1,4 @@ -import { - _sendAdToCreative, getReplier, receiveMessage -} from 'src/secureCreatives.js'; +import {getReplier, receiveMessage} from 'src/secureCreatives.js'; import * as utils from 'src/utils.js'; import {getAdUnits, getBidRequests, getBidResponses} from 'test/fixtures/fixtures.js'; import {auctionManager} from 'src/auctionManager.js'; @@ -8,10 +6,11 @@ import * as auctionModule from 'src/auction.js'; import * as native from 'src/native.js'; import {fireNativeTrackers, getAllAssetsMessage} from 'src/native.js'; import * as events from 'src/events.js'; -import { config as configObj } from 'src/config.js'; +import {config as configObj} from 'src/config.js'; import 'src/prebid.js'; -import { expect } from 'chai'; +import {expect} from 'chai'; +import {handleRender} from '../../../src/adRendering.js'; var CONSTANTS = require('src/constants.json'); @@ -54,37 +53,45 @@ describe('secureCreatives', () => { }); }); - describe('_sendAdToCreative', () => { - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); + describe('handleRender', () => { + let bidResponse, renderFn, result; + beforeEach(() => { + result = null; + renderFn = sinon.stub().callsFake((r) => { result = r; }); + bidResponse = { + adId: 123 + } }); - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); + 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); }); - it('should macro replace ${AUCTION_PRICE} with the winning bid for ad and adUrl', () => { - const oldVal = window.googletag; - const oldapntag = window.apntag; - window.apntag = null - window.googletag = null; - const mockAdObject = { - adId: 'someAdId', - ad: '', - adUrl: 'http://creative.prebid.org/${AUCTION_PRICE}', - width: 300, - height: 250, - renderer: null, - cpm: '1.00', - adUnitCode: 'some_dom_id' - }; - const reply = sinon.spy(); - _sendAdToCreative(mockAdObject, reply); - expect(reply.args[0][0].ad).to.equal(''); - expect(reply.args[0][0].adUrl).to.equal('http://creative.prebid.org/1.00'); - window.googletag = oldVal; - window.apntag = oldapntag; + + ['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'); + }); + }); }); }); diff --git a/test/spec/unit/utils/reducers_spec.js b/test/spec/unit/utils/reducers_spec.js new file mode 100644 index 00000000000..95bf3b74041 --- /dev/null +++ b/test/spec/unit/utils/reducers_spec.js @@ -0,0 +1,124 @@ +import { + tiebreakCompare, + keyCompare, + simpleCompare, + minimum, + maximum, + getHighestCpm, + getOldestHighestCpmBid, getLatestHighestCpmBid, reverseCompare +} from '../../../../src/utils/reducers.js'; +import assert from 'assert'; + +describe('reducers', () => { + describe('simpleCompare', () => { + Object.entries({ + '<': [10, 20, -1], + '===': [123, 123, 0], + '>': [30, -10, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when a ${t} b`, () => { + expect(simpleCompare(a, b)).to.equal(expected); + }) + }) + }); + + describe('keyCompare', () => { + Object.entries({ + '<': [{k: -123}, {k: 0}, -1], + '===': [{k: 0}, {k: 0}, 0], + '>': [{k: 2}, {k: 1}, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when key(a) ${t} key(b)`, () => { + expect(keyCompare(item => item.k)(a, b)).to.equal(expected); + }) + }) + }); + + describe('tiebreakCompare', () => { + Object.entries({ + 'first compare says a < b': [{main: 1, tie: 2}, {main: 2, tie: 1}, -1], + 'first compare says a > b': [{main: 2, tie: 1}, {main: 1, tie: 2}, 1], + 'first compare ties, second says a < b': [{main: 0, tie: 1}, {main: 0, tie: 2}, -1], + 'first compare ties, second says a > b': [{main: 0, tie: 2}, {main: 0, tie: 1}, 1], + 'all compares tie': [{main: 0, tie: 0}, {main: 0, tie: 0}, 0] + }).forEach(([t, [a, b, expected]]) => { + it(`should return ${expected} when ${t}`, () => { + const cmp = tiebreakCompare(keyCompare(item => item.main), keyCompare(item => item.tie)); + expect(cmp(a, b)).to.equal(expected); + }) + }) + }); + + const SAMPLE_ARR = [-10, 20, 20, 123, 400]; + + Object.entries({ + 'minimum': [minimum, ['minimum', -10], ['maximum', 400]], + 'maximum': [maximum, ['maximum', 400], ['minimum', -10]] + }).forEach(([t, [fn, simple, reversed]]) => { + describe(t, () => { + it(`should find ${simple[0]} using simple compare`, () => { + expect(SAMPLE_ARR.reduce(fn(simpleCompare))).to.equal(simple[1]); + }); + it(`should find ${reversed[0]} using reverse compare`, () => { + expect(SAMPLE_ARR.reduce(fn(reverseCompare()))).to.equal(reversed[1]); + }); + }) + }); + + describe('getHighestCpm', function () { + it('should pick the highest cpm', function () { + let a = { + cpm: 2, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 100 + }; + expect(getHighestCpm(a, b)).to.eql(a); + expect(getHighestCpm(b, a)).to.eql(a); + }); + + it('should pick the lowest timeToRespond cpm in case of tie', function () { + let a = { + cpm: 1, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 50 + }; + expect(getHighestCpm(a, b)).to.eql(b); + expect(getHighestCpm(b, a)).to.eql(b); + }); + }); + + describe('getOldestHighestCpmBid', () => { + it('should pick the oldest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getOldestHighestCpmBid(a, b)).to.eql(a); + expect(getOldestHighestCpmBid(b, a)).to.eql(a); + }); + }); + describe('getLatestHighestCpmBid', () => { + it('should pick the latest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getLatestHighestCpmBid(a, b)).to.eql(b); + expect(getLatestHighestCpmBid(b, a)).to.eql(b); + }); + }); +}) diff --git a/test/spec/unit/utils/ttlCollection_spec.js b/test/spec/unit/utils/ttlCollection_spec.js new file mode 100644 index 00000000000..29c6c438855 --- /dev/null +++ b/test/spec/unit/utils/ttlCollection_spec.js @@ -0,0 +1,180 @@ +import {ttlCollection} from '../../../../src/utils/ttlCollection.js'; + +describe('ttlCollection', () => { + it('can add & retrieve items', () => { + const coll = ttlCollection(); + expect(coll.toArray()).to.eql([]); + coll.add(1); + coll.add(2); + expect(coll.toArray()).to.eql([1, 2]); + }); + + it('can clear', () => { + const coll = ttlCollection(); + coll.add('item'); + coll.clear(); + expect(coll.toArray()).to.eql([]); + }); + + it('can be iterated over', () => { + const coll = ttlCollection(); + coll.add('1'); + coll.add('2'); + expect(Array.from(coll)).to.eql(['1', '2']); + }) + + describe('autopurge', () => { + let clock, pms, waitForPromises; + const SLACK = 2000; + beforeEach(() => { + clock = sinon.useFakeTimers(); + pms = []; + waitForPromises = () => Promise.all(pms); + }); + afterEach(() => { + clock.restore(); + }); + + Object.entries({ + 'defer': (value) => { + const pm = Promise.resolve(value); + pms.push(pm); + return pm; + }, + 'do not defer': (value) => value, + }).forEach(([t, resolve]) => { + describe(`when ttl/startTime ${t}`, () => { + let coll; + beforeEach(() => { + coll = ttlCollection({ + startTime: (item) => resolve(item.start == null ? new Date().getTime() : item.start), + ttl: (item) => resolve(item.ttl), + slack: SLACK + }) + }); + + it('should clear items after enough time has passed', () => { + coll.add({no: 'ttl'}); + coll.add({ttl: 1000}); + coll.add({ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 1000}, {ttl: 4000}]); + clock.tick(SLACK + 500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 4000}]); + clock.tick(3000); + expect(coll.toArray()).to.eql([{no: 'ttl'}]); + }); + }); + + it('should not wait too long if a shorter ttl shows up', () => { + coll.add({ttl: 4000}); + coll.add({ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([ + {ttl: 4000} + ]); + }); + }); + + it('should not wait more if later ttls are within slack', () => { + coll.add({start: 0, ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(4000); + coll.add({start: 0, ttl: 5000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should clear items ASAP if they expire in the past', () => { + clock.tick(10000); + coll.add({start: 0, ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + it('should clear items ASAP if they have ttl = 0', () => { + coll.add({ttl: 0}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + describe('refresh', () => { + it('should refresh missing TTLs', () => { + const item = {}; + coll.add(item); + return waitForPromises().then(() => { + item.ttl = 1000; + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([item]); + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(1); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + }); + + it('should refresh existing TTLs', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000); + item.ttl = 4000; + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([item]); + clock.tick(3000); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should discard initial TTL if it does not resolve before a refresh', () => { + let resolveTTL; + const item = { + ttl: new Promise((resolve) => { + resolveTTL = resolve; + }) + }; + coll.add(item); + item.ttl = null; + coll.refresh(); + resolveTTL(1000); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + + it('should discard TTLs on clear', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + coll.clear(); + item.ttl = null; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index e26683074c8..098582c0af6 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,7 +2,9 @@ import {getAdServerTargeting} from 'test/fixtures/fixtures.js'; import {expect} from 'chai'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; -import {deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js'; +import {binarySearch, deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js'; var assert = require('assert'); @@ -39,28 +41,6 @@ describe('Utils', function () { }); }); - describe('tryAppendQueryString', function () { - it('should append query string to existing url', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = 'c'; - - var output = utils.tryAppendQueryString(url, key, value); - - var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; - assert.equal(output, expectedResult); - }); - - it('should return existing url, if the value is empty', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = ''; - - var output = utils.tryAppendQueryString(url, key, value); - assert.equal(output, url); - }); - }); - describe('parseQueryStringParameters', function () { it('should append query string to existing using the input obj', function () { var obj = { @@ -538,72 +518,6 @@ describe('Utils', function () { }); }); - describe('getHighestCpm', function () { - it('should pick the existing highest cpm', function () { - let previous = { - cpm: 2, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), previous); - }); - - it('should pick the new highest cpm', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 2, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the fastest cpm in case of tie', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 50 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the oldest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getOldestHighestCpmBid(previous, current), previous); - }); - - it('should pick the latest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getLatestHighestCpmBid(previous, current), current); - }); - }); - describe('polyfill test', function () { it('should not add polyfill to array', function() { var arr = ['hello', 'world']; @@ -765,43 +679,15 @@ describe('Utils', function () { describe('convertCamelToUnderscore', function () { it('returns converted string value using underscore syntax instead of camelCase', function () { let var1 = 'placementIdTest'; - let test1 = utils.convertCamelToUnderscore(var1); + let test1 = convertCamelToUnderscore(var1); expect(test1).to.equal('placement_id_test'); let var2 = 'my_test_value'; - let test2 = utils.convertCamelToUnderscore(var2); + let test2 = convertCamelToUnderscore(var2); expect(test2).to.equal(var2); }); }); - describe('getAdUnitSizes', function () { - it('returns an empty response when adUnits is undefined', function () { - let sizes = utils.getAdUnitSizes(); - expect(sizes).to.be.undefined; - }); - - it('returns an empty array when invalid data is present in adUnit object', function () { - let sizes = utils.getAdUnitSizes({ sizes: 300 }); - expect(sizes).to.deep.equal([]); - }); - - it('retuns an array of arrays when reading from adUnit.sizes', function () { - let sizes = utils.getAdUnitSizes({ sizes: [300, 250] }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ sizes: [[300, 250], [300, 600]] }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - - it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { - let sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [300, 250] } } }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } } }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - }); - describe('URL helpers', function () { describe('parseUrl()', function () { let parsed; @@ -1233,5 +1119,44 @@ describe('memoize', () => { mem('one', 'three'); expect(mem('one', 'three')).to.eql(['one', 'three']); expect(fn.callCount).to.eql(2); - }) + }); + + describe('binarySearch', () => { + [ + { + arr: [], + tests: [ + ['any', 0] + ] + }, + { + arr: [10], + tests: [ + [5, 0], + [10, 0], + [20, 1], + ], + }, + { + arr: [10, 20, 30, 30, 40], + tests: [ + [5, 0], + [15, 1], + [10, 0], + [30, 2], + [35, 4], + [40, 4], + [100, 5] + ] + } + ].forEach(({arr, tests}) => { + describe(`on ${arr}`, () => { + tests.forEach(([el, pos]) => { + it(`finds index for ${el} => ${pos}`, () => { + expect(binarySearch(arr, el)).to.equal(pos); + }); + }); + }); + }) + }); }) diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 61621c7ec42..35d0a4fef24 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,4 +1,4 @@ -import { isValidVideoBid } from 'src/video.js'; +import {fillVideoDefaults, isValidVideoBid} from 'src/video.js'; import {hook} from '../../src/hook.js'; import {stubAuctionIndex} from '../helpers/indexStub.js'; @@ -7,97 +7,169 @@ describe('video.js', function () { hook.ready(); }); - it('validates valid instream bids', function () { - const bid = { - adId: '456xyz', - vastUrl: 'http://www.example.com/vastUrl', - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + describe('fillVideoDefaults', () => { + function fillDefaults(videoMediaType = {}) { + const adUnit = {mediaTypes: {video: videoMediaType}}; + fillVideoDefaults(adUnit); + return adUnit.mediaTypes.video; + } - it('catches invalid instream bids', function () { - const bid = { - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + describe('should set plcmt = 4 when', () => { + it('context is "outstream"', () => { + expect(fillDefaults({context: 'outstream'})).to.eql({ + context: 'outstream', + plcmt: 4 + }) + }); + [2, 3, 4].forEach(placement => { + it(`placemement is "${placement}"`, () => { + expect(fillDefaults({placement})).to.eql({ + placement, + plcmt: 4 + }); + }) + }); + }); + describe('should set plcmt = 2 when', () => { + [2, 6].forEach(playbackmethod => { + it(`playbackmethod is "${playbackmethod}"`, () => { + expect(fillDefaults({playbackmethod})).to.eql({ + playbackmethod, + plcmt: 2, + }); + }); + }); + }); + describe('should not set plcmt when', () => { + Object.entries({ + 'it was set by pub (context=outstream)': { + expected: 1, + video: { + context: 'outstream', + plcmt: 1 + } + }, + 'it was set by pub (placement=2)': { + expected: 1, + video: { + placement: 2, + plcmt: 1 + } + }, + 'placement not in 2, 3, 4': { + expected: undefined, + video: { + placement: 1 + } + }, + 'it was set by pub (playbackmethod=2)': { + expected: 1, + video: { + plcmt: 1, + playbackmethod: 2 + } + } + }).forEach(([t, {expected, video}]) => { + it(t, () => { + expect(fillDefaults(video).plcmt).to.eql(expected); + }) + }) + }) + }) - it('catches invalid bids when prebid-cache is disabled', function () { - const adUnits = [{ - transactionId: 'au', - bidder: 'vastOnlyVideoBidder', - mediaTypes: {video: {}}, - }]; + describe('isValidVideoBid', () => { + it('validates valid instream bids', function () { + const bid = { + adId: '456xyz', + vastUrl: 'http://www.example.com/vastUrl', + transactionId: 'au' + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); + it('catches invalid instream bids', function () { + const bid = { + transactionId: 'au' + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); - expect(valid).to.equal(false); - }); + it('catches invalid bids when prebid-cache is disabled', function () { + const adUnits = [{ + transactionId: 'au', + bidder: 'vastOnlyVideoBidder', + mediaTypes: {video: {}}, + }]; - it('validates valid outstream bids', function () { - const bid = { - transactionId: 'au', - renderer: { - url: 'render.url', - render: () => true, - } - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); - it('validates valid outstream bids with a publisher defined renderer', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: { - context: 'outstream', + expect(valid).to.equal(false); + }); + + it('validates valid outstream bids', function () { + const bid = { + transactionId: 'au', + renderer: { + url: 'render.url', + render: () => true, } - }, - renderer: { - url: 'render.url', - render: () => true, - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - it('catches invalid outstream bids', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + transactionId: 'au', + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: { + context: 'outstream', + } + }, + renderer: { + url: 'render.url', + render: () => true, + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); + + it('catches invalid outstream bids', function () { + const bid = { + transactionId: 'au', + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); + }) }); diff --git a/test/test_deps.js b/test/test_deps.js index 35713106f8c..c8a3bcc9426 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -4,6 +4,16 @@ window.process = { } }; +window.addEventListener('error', function (ev) { + // eslint-disable-next-line no-console + console.error('Uncaught exception:', ev.error, ev.error?.stack); +}) + +window.addEventListener('unhandledrejection', function (ev) { + // eslint-disable-next-line no-console + console.error('Unhandled rejection:', ev.reason); +}) + require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); diff --git a/webpack.creative.js b/webpack.creative.js new file mode 100644 index 00000000000..7279455e155 --- /dev/null +++ b/webpack.creative.js @@ -0,0 +1,19 @@ +const path = require('path'); + +module.exports = { + mode: 'production', + resolve: { + modules: [ + path.resolve('.'), + 'node_modules' + ], + }, + entry: { + 'creative': { + import: './libraries/creativeRender/crossDomain.js', + }, + }, + output: { + path: path.resolve('./build/creative'), + }, +}