From 9135793288213d1741feeb3d78810f718b568059 Mon Sep 17 00:00:00 2001 From: abazylewicz-id5 <106807984+abazylewicz-id5@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:59:07 +0200 Subject: [PATCH 01/51] ID5 User Id module - use userId storage mechanism to store request nb (#11965) --- modules/id5IdSystem.js | 107 +++------------------- test/spec/modules/id5IdSystem_spec.js | 123 ++++++-------------------- 2 files changed, 39 insertions(+), 191 deletions(-) diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 1f369f5a7a1..95e40337e79 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -13,14 +13,13 @@ import { isPlainObject, logError, logInfo, - logWarn, - safeJSONParse + logWarn } from '../src/utils.js'; import {fetch} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; +import {gppDataHandler, uspDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; import {GreedyPromise} from '../src/utils/promise.js'; import {loadExternalScript} from '../src/adloader.js'; @@ -34,19 +33,12 @@ import {loadExternalScript} from '../src/adloader.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; -const NB_EXP_DAYS = 30; export const ID5_STORAGE_NAME = 'id5id'; -export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`; -const LOCAL_STORAGE = 'html5'; const LOG_PREFIX = 'User ID - ID5 submodule: '; const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid'; const ID5_DOMAIN = 'id5-sync.com'; const TRUE_LINK_SOURCE = 'true-link-id5-sync.com'; -// order the legacy cookie names in reverse priority order so the last -// cookie in the array is the most preferred to use -const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; - export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @@ -231,7 +223,7 @@ export const id5IdSubmodule = { * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {Object} cacheIdObj - existing id, if any - * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. + * @return {IdResponse} A response object that contains id. */ extendId(config, consentData, cacheIdObj) { if (!hasWriteConsentToLocalStorage(consentData)) { @@ -239,10 +231,10 @@ export const id5IdSubmodule = { return cacheIdObj; } - const partnerId = validateConfig(config) ? config.params.partner : 0; - incrementNb(partnerId); - logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj); + if (cacheIdObj) { + cacheIdObj.nbPage = incrementNb(cacheIdObj) + } return cacheIdObj; }, eids: { @@ -401,8 +393,8 @@ export class IdFetchFlow { const params = this.submoduleConfig.params; const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0; const referer = getRefererInfo(); - const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature(); - const nbPage = incrementAndResetNb(params.partner); + const signature = this.cacheIdObj ? this.cacheIdObj.signature : undefined; + const nbPage = incrementNb(this.cacheIdObj); const trueLinkInfo = window.id5Bootstrap ? window.id5Bootstrap.getTrueLinkInfo() : {booted: false}; const data = { @@ -456,7 +448,6 @@ export class IdFetchFlow { #processFetchCallResponse(fetchCallResponse) { try { if (fetchCallResponse.privacy) { - storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); if (window.id5Bootstrap && window.id5Bootstrap.setPrivacy) { window.id5Bootstrap.setPrivacy(fetchCallResponse.privacy); } @@ -507,89 +498,19 @@ function validateConfig(config) { logError(LOG_PREFIX + 'storage required to be set'); return false; } - - // in a future release, we may return false if storage type or name are not set as required - if (config.storage.type !== LOCAL_STORAGE) { - logWarn(LOG_PREFIX + `storage type recommended to be '${LOCAL_STORAGE}'. In a future release this may become a strict requirement`); - } - // in a future release, we may return false if storage type or name are not set as required if (config.storage.name !== ID5_STORAGE_NAME) { - logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'. In a future release this may become a strict requirement`); + logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'.`); } return true; } -export function expDaysStr(expDays) { - return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString(); -} - -export function nbCacheName(partnerId) { - return `${ID5_STORAGE_NAME}_${partnerId}_nb`; -} - -export function storeNbInCache(partnerId, nb) { - storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS); -} - -export function getNbFromCache(partnerId) { - let cacheNb = getFromLocalStorage(nbCacheName(partnerId)); - return (cacheNb) ? parseInt(cacheNb) : 0; -} - -function incrementNb(partnerId) { - const nb = (getNbFromCache(partnerId) + 1); - storeNbInCache(partnerId, nb); - return nb; -} - -function incrementAndResetNb(partnerId) { - const result = incrementNb(partnerId); - storeNbInCache(partnerId, 0); - return result; -} - -function getLegacyCookieSignature() { - let legacyStoredValue; - LEGACY_COOKIE_NAMES.forEach(function (cookie) { - if (storage.getCookie(cookie)) { - legacyStoredValue = safeJSONParse(storage.getCookie(cookie)) || legacyStoredValue; - } - }); - return (legacyStoredValue && legacyStoredValue.signature) || ''; -} - -/** - * This will make sure we check for expiration before accessing local storage - * @param {string} key - */ -export function getFromLocalStorage(key) { - const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`); - // empty string means no expiration set - if (storedValueExp === '') { - return storage.getDataFromLocalStorage(key); - } else if (storedValueExp) { - if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - return storage.getDataFromLocalStorage(key); - } +function incrementNb(cachedObj) { + if (cachedObj && cachedObj.nbPage !== undefined) { + return cachedObj.nbPage + 1; + } else { + return 1; } - // if we got here, then we have an expired item or we didn't set an - // expiration initially somehow, so we need to remove the item from the - // local storage - storage.removeDataFromLocalStorage(key); - return null; -} - -/** - * Ensure that we always set an expiration in local storage since - * by default it's not required - * @param {string} key - * @param {any} value - * @param {number} expDays - */ -export function storeInLocalStorage(key, value, expDays) { - storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); - storage.setDataInLocalStorage(`${key}`, value); } /** diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index eae5fd21310..2703c6681e7 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -93,6 +93,15 @@ describe('ID5 ID System', function () { 'Content-Type': 'application/json' }; + function expDaysStr(expDays) { + return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString(); + } + + function storeInStorage(key, value, expDays) { + id5System.storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); + id5System.storage.setDataInLocalStorage(`${key}`, value); + } + function getId5FetchConfig(partner = ID5_TEST_PARTNER_ID, storageName = id5System.ID5_STORAGE_NAME, storageType = 'html5') { return { name: ID5_MODULE_NAME, @@ -637,8 +646,6 @@ describe('ID5 ID System', function () { it('should call the ID5 server with nb=1 when no stored value exists and reset after', async function () { const xhrServerMock = new XhrServerMock(server); - const TEST_PARTNER_ID = 189; - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(TEST_PARTNER_ID)); // Trigger the fetch but we await on it later const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); @@ -648,28 +655,28 @@ describe('ID5 ID System', function () { expect(requestBody.nbPage).is.eq(1); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - await submoduleResponsePromise; + const response = await submoduleResponsePromise; - expect(id5System.getNbFromCache(TEST_PARTNER_ID)).is.eq(0); + expect(response.nbPage).is.undefined; }); it('should call the ID5 server with incremented nb when stored value exists and reset after', async function () { const xhrServerMock = new XhrServerMock(server); const TEST_PARTNER_ID = 189; const config = getId5FetchConfig(TEST_PARTNER_ID); - id5System.storeNbInCache(TEST_PARTNER_ID, 1); + const storedObj = {...ID5_STORED_OBJ, nbPage: 1}; // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(config, undefined, ID5_STORED_OBJ); + const submoduleResponsePromise = callSubmoduleGetId(config, undefined, storedObj); const fetchRequest = await xhrServerMock.expectFetchRequest(); const requestBody = JSON.parse(fetchRequest.requestBody); expect(requestBody.nbPage).is.eq(2); fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - await submoduleResponsePromise; + const response = await submoduleResponsePromise; - expect(id5System.getNbFromCache(TEST_PARTNER_ID)).is.eq(0); + expect(response.nbPage).is.undefined; }); it('should call the ID5 server with ab_testing object when abTesting is turned on', async function () { @@ -720,45 +727,6 @@ describe('ID5 ID System', function () { await submoduleResponsePromise; }); - it('should store the privacy object from the ID5 server response', async function () { - const xhrServerMock = new XhrServerMock(server); - - // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - - const privacy = { - jurisdiction: 'gdpr', - id5_consent: true - }; - - const fetchRequest = await xhrServerMock.expectFetchRequest(); - const responseObject = utils.deepClone(ID5_JSON_RESPONSE); - responseObject.privacy = privacy; - - fetchRequest.respond(200, responseHeader, JSON.stringify(responseObject)); - await submoduleResponsePromise; - - expect(id5System.getFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME)).is.eq(JSON.stringify(privacy)); - coreStorage.removeDataFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME); - }); - - it('should not store a privacy object if not part of ID5 server response', async function () { - const xhrServerMock = new XhrServerMock(server); - coreStorage.removeDataFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME); - - // Trigger the fetch but we await on it later - const submoduleResponsePromise = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - - const fetchRequest = await xhrServerMock.expectFetchRequest(); - const responseObject = utils.deepClone(ID5_JSON_RESPONSE); - responseObject.privacy = undefined; - - fetchRequest.respond(200, responseHeader, JSON.stringify(responseObject)); - await submoduleResponsePromise; - - expect(id5System.getFromLocalStorage(id5System.ID5_PRIVACY_STORAGE_NAME)).is.null; - }); - describe('with successful external module call', function () { const MOCK_RESPONSE = { ...ID5_JSON_RESPONSE, @@ -926,7 +894,6 @@ describe('ID5 ID System', function () { sinon.stub(events, 'getEvents').returns([]); coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME); coreStorage.removeDataFromLocalStorage(`${id5System.ID5_STORAGE_NAME}_last`); - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(ID5_TEST_PARTNER_ID)); coreStorage.setDataInLocalStorage(id5System.ID5_STORAGE_NAME + '_cst', getConsentHash()); adUnits = [getAdUnitMock()]; }); @@ -934,19 +901,18 @@ describe('ID5 ID System', function () { events.getEvents.restore(); coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME); coreStorage.removeDataFromLocalStorage(`${id5System.ID5_STORAGE_NAME}_last`); - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(ID5_TEST_PARTNER_ID)); coreStorage.removeDataFromLocalStorage(id5System.ID5_STORAGE_NAME + '_cst'); sandbox.restore(); }); it('should add stored ID from cache to bids', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); init(config); setSubmoduleRegistry([id5System.id5IdSubmodule]); config.setConfig(getFetchLocalStorageConfig()); - requestBidsHook(function () { + requestBidsHook(wrapAsyncExpects(done, () => { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); @@ -964,11 +930,11 @@ describe('ID5 ID System', function () { }); }); done(); - }, {adUnits}); + }), {adUnits}); }); it('should add stored EUID from cache to bids', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_EUID), 1); + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_EUID), 1); init(config); setSubmoduleRegistry([id5System.id5IdSubmodule]); @@ -997,7 +963,7 @@ describe('ID5 ID System', function () { }); it('should add stored TRUE_LINK_ID from cache to bids', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_TRUE_LINK), 1); + storeInStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_TRUE_LINK), 1); init(config); setSubmoduleRegistry([id5System.id5IdSubmodule]); @@ -1041,48 +1007,20 @@ describe('ID5 ID System', function () { }, {adUnits}); }); - it('should set nb=1 in cache when no stored nb value exists and cached ID', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); - coreStorage.removeDataFromLocalStorage(id5System.nbCacheName(ID5_TEST_PARTNER_ID)); - - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getFetchLocalStorageConfig()); - - requestBidsHook((adUnitConfig) => { - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(1); - done(); - }, {adUnits}); - }); - - it('should increment nb in cache when stored nb value exists and cached ID', function (done) { - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); - id5System.storeNbInCache(ID5_TEST_PARTNER_ID, 1); - - init(config); - setSubmoduleRegistry([id5System.id5IdSubmodule]); - config.setConfig(getFetchLocalStorageConfig()); - - requestBidsHook(() => { - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(2); - done(); - }, {adUnits}); - }); - it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { const xhrServerMock = new XhrServerMock(server); - const initialLocalStorageValue = JSON.stringify(ID5_STORED_OBJ); - id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, initialLocalStorageValue, 1); - id5System.storeInLocalStorage(`${id5System.ID5_STORAGE_NAME}_last`, id5System.expDaysStr(-1), 1); + let storedObject = ID5_STORED_OBJ; + storedObject.nbPage = 1; + const initialLocalStorageValue = JSON.stringify(storedObject); + storeInStorage(id5System.ID5_STORAGE_NAME, initialLocalStorageValue, 1); + storeInStorage(`${id5System.ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); - id5System.storeNbInCache(ID5_TEST_PARTNER_ID, 1); let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; id5Config.userSync.auctionDelay = 0; // do not trigger callback before auction init(config); setSubmoduleRegistry([id5System.id5IdSubmodule]); config.setConfig(id5Config); - return new Promise((resolve) => { requestBidsHook(() => { resolve(); @@ -1095,18 +1033,7 @@ describe('ID5 ID System', function () { const requestBody = JSON.parse(request.requestBody); expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); expect(requestBody.nbPage).is.eq(2); - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); request.respond(200, HEADERS_CONTENT_TYPE_JSON, JSON.stringify(ID5_JSON_RESPONSE)); - - return new Promise(function (resolve) { - (function waitForCondition() { - if (id5System.getFromLocalStorage(id5System.ID5_STORAGE_NAME) !== initialLocalStorageValue) return resolve(); - setTimeout(waitForCondition, 30); - })(); - }); - }).then(() => { - expect(decodeURIComponent(id5System.getFromLocalStorage(id5System.ID5_STORAGE_NAME))).is.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(id5System.getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); }); }); }); From 9d5bedef42fe6cedd20cce43cd993dea620d492c Mon Sep 17 00:00:00 2001 From: Chris Huie Date: Wed, 24 Jul 2024 10:06:39 -0600 Subject: [PATCH 02/51] Readme : fix broken link to docs (#12031) * Readme : fix broken link to docs * Update README.md * Update README.md --------- Co-authored-by: Patrick McCann --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f890f055104..609baf82c08 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build Status](https://circleci.com/gh/prebid/Prebid.js.svg?style=svg)](https://circleci.com/gh/prebid/Prebid.js) -[![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](https://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") [![Coverage Status](https://coveralls.io/repos/github/prebid/Prebid.js/badge.svg)](https://coveralls.io/github/prebid/Prebid.js) # Prebid.js @@ -7,8 +7,8 @@ > A free and open source library for publishers to quickly implement header bidding. This README is for developers who want to contribute to Prebid.js. -Additional documentation can be found at [the Prebid homepage](http://prebid.org). -Working examples can be found in [the developer docs](http://prebid.org/dev-docs/getting-started.html). +Additional documentation can be found at [the Prebid.js documentation homepage](https://docs.prebid.org/prebid/prebidjs.html). +Working examples can be found in [the developer docs](https://prebid.org/dev-docs/getting-started.html). Prebid.js is open source software that is offered for free as a convenience. While it is designed to help companies address legal requirements associated with header bidding, we cannot and do not warrant that your use of Prebid.js will satisfy legal requirements. You are solely responsible for ensuring that your use of Prebid.js complies with all applicable laws. We strongly encourage you to obtain legal advice when using Prebid.js to ensure your implementation complies with all laws where you operate. @@ -374,7 +374,7 @@ The results will be in *Note*: Starting in June 2016, all pull requests to Prebid.js need to include tests with greater than 80% code coverage before they can be merged. For more information, see [#421](https://github.com/prebid/Prebid.js/issues/421). -For instructions on writing tests for Prebid.js, see [Testing Prebid.js](http://prebid.org/dev-docs/testing-prebid.html). +For instructions on writing tests for Prebid.js, see [Testing Prebid.js](https://prebid.org/dev-docs/testing-prebid.html). ### Supported Browsers From 6b2b59ea135a6bfdea569374af0f77a2ddf09e23 Mon Sep 17 00:00:00 2001 From: Olivier Date: Thu, 25 Jul 2024 23:24:03 +0200 Subject: [PATCH 03/51] Core: ORTB video params validation (work on dupe) (#11970) * Core: Video: add ORTB video params validation * AdagioBidAdapter: use video helper for ortb fields validation * Core: Video: improve validateOrtbVideoFields() * Core: Video: use compacted Map for ORTB_VIDEO_PARAMS --- libraries/ortbConverter/processors/video.js | 26 +---- modules/adagioBidAdapter.js | 60 ++--------- src/prebid.js | 3 +- src/video.js | 79 ++++++++++++++- test/spec/modules/adagioBidAdapter_spec.js | 1 - test/spec/video_spec.js | 104 +++++++++++++++++++- 6 files changed, 195 insertions(+), 78 deletions(-) diff --git a/libraries/ortbConverter/processors/video.js b/libraries/ortbConverter/processors/video.js index caa855566eb..85ab1cd6ecf 100644 --- a/libraries/ortbConverter/processors/video.js +++ b/libraries/ortbConverter/processors/video.js @@ -1,30 +1,7 @@ import {deepAccess, isEmpty, logWarn, mergeDeep, sizesToSizeTuples, sizeTupleToRtbSize} from '../../../src/utils.js'; import {VIDEO} from '../../../src/mediaTypes.js'; -// parameters that share the same name & semantics between pbjs adUnits and imp.video -const ORTB_VIDEO_PARAMS = new Set([ - 'pos', - 'placement', - 'plcmt', - 'api', - 'mimes', - 'protocols', - 'playbackmethod', - 'minduration', - 'maxduration', - 'w', - 'h', - 'startdelay', - 'placement', - 'linearity', - 'skip', - 'skipmin', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackend' -]); +import {ORTB_VIDEO_PARAMS} from '../../../src/video.js'; export function fillVideoImp(imp, bidRequest, context) { if (context.mediaType && context.mediaType !== VIDEO) return; @@ -32,6 +9,7 @@ export function fillVideoImp(imp, bidRequest, context) { const videoParams = deepAccess(bidRequest, 'mediaTypes.video'); if (!isEmpty(videoParams)) { const video = Object.fromEntries( + // Parameters that share the same name & semantics between pbjs adUnits and imp.video Object.entries(videoParams) .filter(([name]) => ORTB_VIDEO_PARAMS.has(name)) ); diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 9bcebf9205e..cb125d4446e 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -8,9 +8,7 @@ import { getDNT, getWindowSelf, isArray, - isArrayOfNums, isFn, - isInteger, isNumber, isStr, logError, @@ -18,7 +16,7 @@ import { logWarn, } from '../src/utils.js'; import { getRefererInfo, parseDomain } from '../src/refererDetection.js'; -import { OUTSTREAM } from '../src/video.js'; +import { OUTSTREAM, validateOrtbVideoFields } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; import { _ADAGIO } from '../libraries/adagioUtils/adagioUtils.js'; import { config } from '../src/config.js'; @@ -40,39 +38,6 @@ export const BB_RENDERER_URL = `https://${BB_PUBLICATION}.bbvms.com/r/$RENDERER. const CURRENCY = 'USD'; -// 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), - 'maxduration': (value) => isInteger(value), - 'protocols': (value) => isArrayOfNums(value), - 'w': (value) => isInteger(value), - 'h': (value) => isInteger(value), - 'startdelay': (value) => isInteger(value), - 'placement': (value) => { - logWarn(LOG_PREFIX, 'The OpenRTB video param `placement` is deprecated and should not be used anymore.'); - return isInteger(value) - }, - 'plcmt': (value) => isInteger(value), - 'linearity': (value) => isInteger(value), - 'skip': (value) => [1, 0].includes(value), - 'skipmin': (value) => isInteger(value), - 'skipafter': (value) => isInteger(value), - 'sequence': (value) => isInteger(value), - 'battr': (value) => isArrayOfNums(value), - 'maxextended': (value) => isInteger(value), - 'minbitrate': (value) => isInteger(value), - 'maxbitrate': (value) => isInteger(value), - 'boxingallowed': (value) => isInteger(value), - 'playbackmethod': (value) => isArrayOfNums(value), - 'playbackend': (value) => isInteger(value), - 'delivery': (value) => isArrayOfNums(value), - 'pos': (value) => isInteger(value), - 'api': (value) => isArrayOfNums(value) -}; - /** * Returns the window.ADAGIO global object used to store Adagio data. * This object is created in window.top if possible, otherwise in window.self. @@ -186,6 +151,12 @@ function _getEids(bidRequest) { } } +/** + * Merge and compute video params set at mediaTypes and bidder params level + * + * @param {object} bidRequest - copy of the original bidRequest object. + * @returns {void} + */ function _buildVideoBidRequest(bidRequest) { const videoAdUnitParams = deepAccess(bidRequest, 'mediaTypes.video', {}); const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); @@ -206,22 +177,11 @@ function _buildVideoBidRequest(bidRequest) { }; if (videoParams.context && videoParams.context === OUTSTREAM) { - bidRequest.mediaTypes.video.playerName = getPlayerName(bidRequest); + videoParams.playerName = getPlayerName(bidRequest); } - // Only whitelisted OpenRTB options need to be validated. - // Other options will still remain in the `mediaTypes.video` object - // sent in the ad-request, but will be ignored by the SSP. - Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => { - if (videoParams.hasOwnProperty(paramName)) { - if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) { - bidRequest.mediaTypes.video[paramName] = videoParams[paramName]; - } else { - delete bidRequest.mediaTypes.video[paramName]; - logWarn(`${LOG_PREFIX} The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); - } - } - }); + bidRequest.mediaTypes.video = videoParams; + validateOrtbVideoFields(bidRequest); } function _parseNativeBidResponse(bid) { diff --git a/src/prebid.js b/src/prebid.js index e91af3e4d04..c92ab8f5a89 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -41,7 +41,7 @@ import {enrichFPD} from './fpd/enrichment.js'; import {allConsent} from './consentHandler.js'; import {insertLocatorFrame, renderAdDirect} from './adRendering.js'; import {getHighestCpm} from './utils/reducers.js'; -import {fillVideoDefaults} from './video.js'; +import {fillVideoDefaults, validateOrtbVideoFields} from './video.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; @@ -134,6 +134,7 @@ function validateVideoMediaType(adUnit) { delete validatedAdUnit.mediaTypes.video.playerSize; } } + validateOrtbVideoFields(validatedAdUnit); return validatedAdUnit; } diff --git a/src/video.js b/src/video.js index f8de2b98861..9be9adec4c5 100644 --- a/src/video.js +++ b/src/video.js @@ -1,4 +1,4 @@ -import {deepAccess, logError} from './utils.js'; +import {deepAccess, isArrayOfNums, isInteger, isNumber, isPlainObject, isStr, logError, logWarn} from './utils.js'; import {config} from '../src/config.js'; import {hook} from './hook.js'; import {auctionManager} from './auctionManager.js'; @@ -6,6 +6,47 @@ import {auctionManager} from './auctionManager.js'; export const OUTSTREAM = 'outstream'; export const INSTREAM = 'instream'; +/** + * List of OpenRTB 2.x video object properties with simple validators. + * Not included: `companionad`, `durfloors`, `ext` + * reference: https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md + */ +export const ORTB_VIDEO_PARAMS = new Map([ + [ 'mimes', value => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string') ], + [ 'minduration', isInteger ], + [ 'maxduration', isInteger ], + [ 'startdelay', isInteger ], + [ 'maxseq', isInteger ], + [ 'poddur', isInteger ], + [ 'protocols', isArrayOfNums ], + [ 'w', isInteger ], + [ 'h', isInteger ], + [ 'podid', isStr ], + [ 'podseq', isInteger ], + [ 'rqddurs', isArrayOfNums ], + [ 'placement', isInteger ], // deprecated, see plcmt + [ 'plcmt', isInteger ], + [ 'linearity', isInteger ], + [ 'skip', value => [1, 0].includes(value) ], + [ 'skipmin', isInteger ], + [ 'skipafter', isInteger ], + [ 'sequence', isInteger ], // deprecated + [ 'slotinpod', isInteger ], + [ 'mincpmpersec', isNumber ], + [ 'battr', isArrayOfNums ], + [ 'maxextended', isInteger ], + [ 'minbitrate', isInteger ], + [ 'maxbitrate', isInteger ], + [ 'boxingallowed', isInteger ], + [ 'playbackmethod', isArrayOfNums ], + [ 'playbackend', isInteger ], + [ 'delivery', isArrayOfNums ], + [ 'pos', isInteger ], + [ 'api', isArrayOfNums ], + [ 'companiontype', isArrayOfNums ], + [ 'poddedupe', isArrayOfNums ] +]); + export function fillVideoDefaults(adUnit) { const video = adUnit?.mediaTypes?.video; if (video != null && video.plcmt == null) { @@ -17,6 +58,42 @@ export function fillVideoDefaults(adUnit) { } } +/** + * validateOrtbVideoFields mutates the `adUnit.mediaTypes.video` object by removing invalid ortb properties (default). + * The onInvalidParam callback can be used to handle invalid properties differently. + * Other properties are ignored and kept as is. + * + * @param {Object} adUnit - The adUnit object. + * @param {Function} onInvalidParam - The callback function to be called with key, value, and adUnit. + * @returns {void} + */ +export function validateOrtbVideoFields(adUnit, onInvalidParam) { + const videoParams = adUnit?.mediaTypes?.video; + + if (!isPlainObject(videoParams)) { + logWarn(`validateOrtbVideoFields: videoParams must be an object.`); + return; + } + + if (videoParams != null) { + Object.entries(videoParams) + .forEach(([key, value]) => { + if (!ORTB_VIDEO_PARAMS.has(key)) { + return + } + const isValid = ORTB_VIDEO_PARAMS.get(key)(value); + if (!isValid) { + if (typeof onInvalidParam === 'function') { + onInvalidParam(key, value, adUnit); + } else { + delete videoParams[key]; + logWarn(`Invalid prop in adUnit "${adUnit.code}": Invalid value for mediaTypes.video.${key} ORTB property. The property has been removed.`); + } + } + }); + } +} + /** * @typedef {object} VideoBid * @property {string} adId id of the bid diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 629771de9d6..75b0635bbef 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -603,7 +603,6 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); expect(requests).to.have.lengthOf(1); expect(requests[0].data.adUnits[0].mediaTypes.video).to.deep.equal(expected); - sinon.assert.calledTwice(utils.logWarn.withArgs(sinon.match(new RegExp(/^Adagio: The OpenRTB/)))); }); }); diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 3252c58c687..18955771049 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,12 +1,26 @@ -import {fillVideoDefaults, isValidVideoBid} from 'src/video.js'; +import {fillVideoDefaults, isValidVideoBid, validateOrtbVideoFields} from 'src/video.js'; import {hook} from '../../src/hook.js'; import {stubAuctionIndex} from '../helpers/indexStub.js'; +import * as utils from '../../src/utils.js'; describe('video.js', function () { + let sandbox; + let utilsMock; + before(() => { hook.ready(); }); + beforeEach(() => { + sandbox = sinon.createSandbox(); + utilsMock = sandbox.mock(utils); + }) + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }); + describe('fillVideoDefaults', () => { function fillDefaults(videoMediaType = {}) { const adUnit = {mediaTypes: {video: videoMediaType}}; @@ -77,6 +91,94 @@ describe('video.js', function () { }) }) + describe('validateOrtbVideoFields', () => { + it('remove incorrect ortb properties, and keep non ortb ones', () => { + sandbox.spy(utils, 'logWarn'); + + const mt = { + content: 'outstream', + + mimes: ['video/mp4'], + minduration: 5, + maxduration: 15, + startdelay: 0, + maxseq: 0, + poddur: 0, + protocols: [7], + w: 600, + h: 480, + podid: 'an-id', + podseq: 0, + rqddurs: [5], + placement: 1, + plcmt: 1, + linearity: 1, + skip: 0, + skipmin: 3, + skipafter: 3, + sequence: 0, + slotinpod: 0, + mincpmpersec: 2.5, + battr: [6, 7], + maxextended: 0, + minbitrate: 800, + maxbitrate: 1000, + boxingallowed: 1, + playbackmethod: [1], + playbackend: 1, + delivery: [2], + pos: 0, + api: 6, // -- INVALID + companiontype: [1, 2, 3], + poddedupe: [1], + + otherOne: 'test', + }; + + const expected = {...mt}; + delete expected.api; + + const adUnit = { + code: 'adUnitCode', + mediaTypes: { video: mt } + }; + validateOrtbVideoFields(adUnit); + + expect(adUnit.mediaTypes.video).to.eql(expected); + sinon.assert.callCount(utils.logWarn, 1); + }); + + it('Early return when 1st param is not a plain object', () => { + sandbox.spy(utils, 'logWarn'); + + validateOrtbVideoFields(); + validateOrtbVideoFields([]); + validateOrtbVideoFields(null); + validateOrtbVideoFields('hello'); + validateOrtbVideoFields(() => {}); + + sinon.assert.callCount(utils.logWarn, 5); + }); + + it('Calls onInvalidParam when a property is invalid', () => { + const onInvalidParam = sandbox.spy(); + const adUnit = { + code: 'adUnitCode', + mediaTypes: { + video: { + content: 'outstream', + mimes: ['video/mp4'], + api: 6 + } + } + }; + validateOrtbVideoFields(adUnit, onInvalidParam); + + sinon.assert.calledOnce(onInvalidParam); + sinon.assert.calledWith(onInvalidParam, 'api', 6, adUnit); + }); + }) + describe('isValidVideoBid', () => { it('validates valid instream bids', function () { const bid = { From e4efe07a15fce200c45d57911661703c20ae1ba8 Mon Sep 17 00:00:00 2001 From: maelmrgt <77864748+maelmrgt@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:26:23 +0200 Subject: [PATCH 04/51] Greenbids RTD provider: debug flag (#12037) * feat(rtd): add flag to force filtering of rtd module * creating test_branch * add test file * feat(rtd): add debug flag to remove bidders from auction * del test file * bump * review --- modules/greenbidsAnalyticsAdapter.js | 11 +++++++---- modules/greenbidsRtdProvider.js | 20 +++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 0aac98b453e..156ae0332a2 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {deepClone, generateUUID, logError, logInfo, logWarn, getParameterByName} const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '2.3.0'; +export const ANALYTICS_VERSION = '2.3.1'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; @@ -197,9 +197,12 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); - this.sendEventMessage('/', - this.createBidMessage(auctionEndArgs, cachedAuction) - ); + const isFilteringForced = getParameterByName('greenbids_force_filtering'); + if (!isFilteringForced) { + this.sendEventMessage('/', + this.createBidMessage(auctionEndArgs, cachedAuction) + ) + }; }, handleBidTimeout(timeoutBids) { timeoutBids.forEach((bid) => { diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index 5496fc71c4e..e7423abf115 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,11 +1,11 @@ -import { logError, deepClone, generateUUID, deepSetValue, deepAccess } from '../src/utils.js'; +import { logError, logInfo, logWarn, deepClone, generateUUID, deepSetValue, deepAccess, getParameterByName } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.js'; const MODULE_NAME = 'greenbidsRtdProvider'; -const MODULE_VERSION = '2.0.0'; +const MODULE_VERSION = '2.0.1'; const ENDPOINT = 'https://t.greenbids.ai'; const rtdOptions = {}; @@ -46,6 +46,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { function createPromise(reqBidsConfigObj, greenbidsId) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { + logWarn('GreenbidsRtdProvider: Greenbids API timeout, skipping shaping'); resolve(reqBidsConfigObj); }, rtdOptions.timeout); ajax( @@ -57,6 +58,7 @@ function createPromise(reqBidsConfigObj, greenbidsId) { }, error: () => { clearTimeout(timeoutId); + logWarn('GreenbidsRtdProvider: Greenbids API response error, skipping shaping'); resolve(reqBidsConfigObj); }, }, @@ -73,11 +75,16 @@ function createPromise(reqBidsConfigObj, greenbidsId) { function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) { clearTimeout(timeoutId); - const responseAdUnits = JSON.parse(response); - updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); + try { + const responseAdUnits = JSON.parse(response); + updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); + } catch (e) { + logWarn('GreenbidsRtdProvider: Greenbids API response parsing error, skipping shaping'); + } } function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { + const isFilteringForced = getParameterByName('greenbids_force_filtering'); adUnits.forEach((adUnit) => { const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); if (matchingAdUnit) { @@ -86,7 +93,10 @@ function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { keptInAuction: matchingAdUnit.bidders, isExploration: matchingAdUnit.isExploration }); - if (!matchingAdUnit.isExploration) { + if (isFilteringForced) { + adUnit.bids = []; + logInfo('Greenbids Rtd: filtering flag detected, forcing filtering of Rtd module.'); + } else if (!matchingAdUnit.isExploration) { removeFalseBidders(adUnit, matchingAdUnit); } } From c68d96c33fddd819b45f79c8f8dacb50ff6a5ece Mon Sep 17 00:00:00 2001 From: Chris Huie Date: Thu, 25 Jul 2024 15:27:59 -0600 Subject: [PATCH 05/51] Update default to maintenance (#12022) --- .github/release-drafter.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a3246cffd6d..cb1a634f613 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,14 +3,15 @@ name-template: 'Prebid $RESOLVED_VERSION Release' tag-template: '$RESOLVED_VERSION' categories: - title: '🚀 New Features' - label: 'feature' + label: 'feature' - title: '🛠 Maintenance' - label: 'maintenance' + label: 'maintenance' - title: '🐛 Bug Fixes' - labels: - - 'fix' - - 'bugfix' - - 'bug' + labels: + - 'fix' + - 'bugfix' + - 'bug' + default: maintenance change-template: '- $TITLE (#$NUMBER)' version-resolver: major: From 530cf2bb1481ca3f173cea71bee50cadf1eb93eb Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Thu, 25 Jul 2024 17:30:11 -0400 Subject: [PATCH 06/51] Revert "Update default to maintenance (#12022)" (#12040) This reverts commit c68d96c33fddd819b45f79c8f8dacb50ff6a5ece. --- .github/release-drafter.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index cb1a634f613..a3246cffd6d 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,15 +3,14 @@ name-template: 'Prebid $RESOLVED_VERSION Release' tag-template: '$RESOLVED_VERSION' categories: - title: '🚀 New Features' - label: 'feature' + label: 'feature' - title: '🛠 Maintenance' - label: 'maintenance' + label: 'maintenance' - title: '🐛 Bug Fixes' - labels: - - 'fix' - - 'bugfix' - - 'bug' - default: maintenance + labels: + - 'fix' + - 'bugfix' + - 'bug' change-template: '- $TITLE (#$NUMBER)' version-resolver: major: From fa78a8b41abfb5d3e3ad4032bbe0b77565f65d67 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Thu, 25 Jul 2024 19:39:01 -0400 Subject: [PATCH 07/51] Update release-drafter.yml (#12041) --- .github/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index a3246cffd6d..5876dfa0138 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,6 +1,10 @@ name-template: 'Prebid $RESOLVED_VERSION Release' tag-template: '$RESOLVED_VERSION' +autolabeler: + - label: 'maintenance' + title: + - '/^(?!.*(bug|initial|release|fix)).*$/i' categories: - title: '🚀 New Features' label: 'feature' From 5cefb01069b694e89d18b1035e15f67abef47f98 Mon Sep 17 00:00:00 2001 From: Luca Corbo Date: Fri, 26 Jul 2024 03:33:04 +0200 Subject: [PATCH 08/51] WURFL RTD submodule: initial version (#11840) * WURFL Rtd Provider: initial version * WURFL Rtd Provider: import fetch method from ajax.js module * WURFL Rtd Provider: unit tests * WURFL Rtd Provider: list wurflRtdProvider in the .submodules.json file * WURFL Rtd Provider: remove wurfl from adloader.js * WURFL Rtd Provider: update to use loadExternalScript * WURFL Rtd Provider: update to use sendBeacon from ajax.js --- .../gpt/wurflRtdProvider_example.html | 106 ++++++ modules/.submodules.json | 3 +- modules/wurflRtdProvider.js | 213 ++++++++++++ modules/wurflRtdProvider.md | 67 ++++ src/adloader.js | 1 + test/spec/modules/wurflRtdProvider_spec.js | 324 ++++++++++++++++++ 6 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 integrationExamples/gpt/wurflRtdProvider_example.html create mode 100644 modules/wurflRtdProvider.js create mode 100644 modules/wurflRtdProvider.md create mode 100644 test/spec/modules/wurflRtdProvider_spec.js diff --git a/integrationExamples/gpt/wurflRtdProvider_example.html b/integrationExamples/gpt/wurflRtdProvider_example.html new file mode 100644 index 00000000000..f2bfe7f76b7 --- /dev/null +++ b/integrationExamples/gpt/wurflRtdProvider_example.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + \ No newline at end of file diff --git a/modules/.submodules.json b/modules/.submodules.json index ea128e91579..6b4b0d21d54 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -100,7 +100,8 @@ "sirdataRtdProvider", "symitriDapRtdProvider", "timeoutRtdProvider", - "weboramaRtdProvider" + "weboramaRtdProvider", + "wurflRtdProvider" ], "fpdModule": [ "validationFpdModule", diff --git a/modules/wurflRtdProvider.js b/modules/wurflRtdProvider.js new file mode 100644 index 00000000000..94bb8c6440a --- /dev/null +++ b/modules/wurflRtdProvider.js @@ -0,0 +1,213 @@ +import { submodule } from '../src/hook.js'; +import { fetch, sendBeacon } from '../src/ajax.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { + mergeDeep, + prefixLog, +} from '../src/utils.js'; + +// Constants +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = 'wurfl'; + +// WURFL_JS_HOST is the host for the WURFL service endpoints +const WURFL_JS_HOST = 'https://prebid.wurflcloud.com'; +// WURFL_JS_ENDPOINT_PATH is the path for the WURFL.js endpoint used to load WURFL data +const WURFL_JS_ENDPOINT_PATH = '/wurfl.js'; +// STATS_ENDPOINT_PATH is the path for the stats endpoint used to send analytics data +const STATS_ENDPOINT_PATH = '/v1/prebid/stats'; + +const logger = prefixLog('[WURFL RTD Submodule]'); + +// enrichedBidders holds a list of prebid bidder names, of bidders which have been +// injected with WURFL data +const enrichedBidders = new Set(); + +/** + * init initializes the WURFL RTD submodule + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +const init = (config, userConsent) => { + logger.logMessage('initialized'); + return true; +} + +/** + * getBidRequestData enriches the OpenRTB 2.0 device data with WURFL data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +const getBidRequestData = (reqBidsConfigObj, callback, config, userConsent) => { + const altHost = config.params?.altHost ?? null; + const isDebug = config.params?.debug ?? false; + + const bidders = new Set(); + reqBidsConfigObj.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + bidders.add(bid.bidder); + }); + }); + + let host = WURFL_JS_HOST; + if (altHost) { + host = altHost; + } + + const url = new URL(host); + url.pathname = WURFL_JS_ENDPOINT_PATH; + + if (isDebug) { + url.searchParams.set('debug', 'true') + } + + url.searchParams.set('mode', 'prebid') + logger.logMessage('url', url.toString()); + + try { + loadExternalScript(url.toString(), MODULE_NAME, () => { + logger.logMessage('script injected'); + window.WURFLPromises.complete.then((res) => { + logger.logMessage('received data', res); + if (!res.wurfl_pbjs) { + logger.logError('invalid WURFL.js for Prebid response'); + } else { + enrichBidderRequests(reqBidsConfigObj, bidders, res); + } + callback(); + }); + }); + } catch (err) { + logger.logError(err); + callback(); + } +} + +/** + * enrichBidderRequests enriches the OpenRTB 2.0 device data with WURFL data for Business Edition + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Array} bidders List of bidders + * @param {Object} wjsResponse WURFL.js response + */ +function enrichBidderRequests(reqBidsConfigObj, bidders, wjsResponse) { + const authBidders = wjsResponse.wurfl_pbjs?.authorized_bidders ?? {}; + const caps = wjsResponse.wurfl_pbjs?.caps ?? []; + + bidders.forEach((bidderCode) => { + if (bidderCode in authBidders) { + // inject WURFL data + enrichedBidders.add(bidderCode); + const data = bidderData(wjsResponse.WURFL, caps, authBidders[bidderCode]); + logger.logMessage(`injecting data for ${bidderCode}: `, data); + enrichBidderRequest(reqBidsConfigObj, bidderCode, data); + return; + } + // inject WURFL low entropy data + const data = lowEntropyData(wjsResponse.WURFL, wjsResponse.wurfl_pbjs?.low_entropy_caps); + logger.logMessage(`injecting low entropy data for ${bidderCode}: `, data); + enrichBidderRequest(reqBidsConfigObj, bidderCode, data); + }); +} + +/** + * bidderData returns the WURFL data for a bidder + * @param {Object} wurflData WURFL data + * @param {Array} caps Capability list + * @param {Array} filter Filter list + * @returns {Object} Bidder data + */ +export const bidderData = (wurflData, caps, filter) => { + const data = {}; + caps.forEach((cap, index) => { + if (!filter.includes(index)) { + return; + } + if (cap in wurflData) { + data[cap] = wurflData[cap]; + } + }); + return data; +} + +/** + * lowEntropyData returns the WURFL low entropy data + * @param {Object} wurflData WURFL data + * @param {Array} lowEntropyCaps Low entropy capability list + * @returns {Object} Bidder data + */ +export const lowEntropyData = (wurflData, lowEntropyCaps) => { + const data = {}; + lowEntropyCaps.forEach((cap, _) => { + let value = wurflData[cap]; + if (cap == 'complete_device_name') { + value = value.replace(/Apple (iP(hone|ad|od)).*/, 'Apple iP$2'); + } + data[cap] = value; + }); + return data; +} + +/** + * enrichBidderRequest enriches the bidder request with WURFL data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {String} bidderCode Bidder code + * @param {Object} wurflData WURFL data + */ +export const enrichBidderRequest = (reqBidsConfigObj, bidderCode, wurflData) => { + const ortb2data = { + 'device': { + 'ext': { + 'wurfl': wurflData, + } + }, + }; + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: ortb2data }); +} + +/** + * onAuctionEndEvent is called when the auction ends + * @param {Object} auctionDetails Auction details + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +function onAuctionEndEvent(auctionDetails, config, userConsent) { + const altHost = config.params?.altHost ?? null; + + let host = WURFL_JS_HOST; + if (altHost) { + host = altHost; + } + + const url = new URL(host); + url.pathname = STATS_ENDPOINT_PATH; + + if (enrichedBidders.size === 0) { + return; + } + + var payload = JSON.stringify({ bidders: [...enrichedBidders] }); + const sentBeacon = sendBeacon(url.toString(), payload); + if (sentBeacon) { + return; + } + + fetch(url.toString(), { + method: 'POST', + body: payload, + mode: 'no-cors', + keepalive: true + }); +} + +// The WURFL submodule +export const wurflSubmodule = { + name: MODULE_NAME, + init, + getBidRequestData, + onAuctionEndEvent, +} + +// Register the WURFL submodule as submodule of realTimeData +submodule(REAL_TIME_MODULE, wurflSubmodule); diff --git a/modules/wurflRtdProvider.md b/modules/wurflRtdProvider.md new file mode 100644 index 00000000000..d656add3543 --- /dev/null +++ b/modules/wurflRtdProvider.md @@ -0,0 +1,67 @@ +# WURFL Real-time Data Submodule + +## Overview + + Module Name: WURFL Rtd Provider + Module Type: Rtd Provider + Maintainer: prebid@scientiamobile.com + +## Description + +The WURFL RTD module enriches the OpenRTB 2.0 device data with [WURFL data](https://www.scientiamobile.com/wurfl-js-business-edition-at-the-intersection-of-javascript-and-enterprise/). +The module sets the WURFL data in `device.ext.wurfl` and all the bidder adapters will always receive the low entry capabilites like `is_mobile`, `complete_device_name` and `form_factor`. + +For a more detailed analysis bidders can subscribe to detect iPhone and iPad models and receive additional [WURFL device capabilities](https://www.scientiamobile.com/capabilities/?products%5B%5D=wurfl-js). + +## User-Agent Client Hints + +WURFL.js is fully compatible with Chromium's User-Agent Client Hints (UA-CH) initiative. If User-Agent Client Hints are absent in the HTTP headers that WURFL.js receives, the service will automatically fall back to using the User-Agent Client Hints' JS API to fetch [high entropy client hint values](https://wicg.github.io/ua-client-hints/#getHighEntropyValues) from the client device. However, we recommend that you explicitly opt-in/advertise support for User-Agent Client Hints on your website and delegate them to the WURFL.js service for the fastest detection experience. Our documentation regarding implementing User-Agent Client Hint support [is available here](https://docs.scientiamobile.com/guides/implementing-useragent-clienthints). + +## Usage + +### Build +``` +gulp build --modules="wurflRtdProvider,appnexusBidAdapter,..." +``` + +### Configuration + +Use `setConfig` to instruct Prebid.js to initilize the WURFL RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +var TIMEOUT = 1000; +pbjs.setConfig({ + realTimeData: { + auctionDelay: TIMEOUT, + dataProviders: [{ + name: 'wurfl', + waitForIt: true, + params: { + debug: false + } + }] + } +}); +``` + +### Parameters + +| Name | Type | Description | Default | +| :------------------------ | :------------ | :--------------------------------------------------------------- |:----------------- | +| name | String | Real time data module name | Always 'wurfl' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | | +| params.altHost | String | Alternate host to connect to WURFL.js | | +| params.debug | Boolean | Enable debug | `false` | + +## Testing + +To view an example of how the WURFL RTD module works : + +`gulp serve --modules=wurflRtdProvider,appnexusBidAdapter` + +and then point your browser at: + +`http://localhost:9999/integrationExamples/gpt/wurflRtdProvider_example.html` diff --git a/src/adloader.js b/src/adloader.js index 79ea6e017bb..d1bc881adb5 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -32,6 +32,7 @@ const _approvedLoadExternalJSList = [ 'dynamicAdBoost', '51Degrees', 'symitridap', + 'wurfl', // UserId Submodules 'justtag', 'tncId', diff --git a/test/spec/modules/wurflRtdProvider_spec.js b/test/spec/modules/wurflRtdProvider_spec.js new file mode 100644 index 00000000000..5b1cc5b751f --- /dev/null +++ b/test/spec/modules/wurflRtdProvider_spec.js @@ -0,0 +1,324 @@ +import { + bidderData, + enrichBidderRequest, + lowEntropyData, + wurflSubmodule, +} from 'modules/wurflRtdProvider'; +import * as ajaxModule from 'src/ajax'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +describe('wurflRtdProvider', function () { + describe('wurflSubmodule', function () { + const altHost = 'http://example.local/wurfl.js'; + const wurfl_pbjs = { + low_entropy_caps: ['complete_device_name', 'form_factor', 'is_mobile'], + caps: [ + 'advertised_browser', + 'advertised_browser_version', + 'advertised_device_os', + 'advertised_device_os_version', + 'brand_name', + 'complete_device_name', + 'form_factor', + 'is_app_webview', + 'is_full_desktop', + 'is_mobile', + 'is_robot', + 'is_smartphone', + 'is_smarttv', + 'is_tablet', + 'manufacturer_name', + 'marketing_name' + ], + authorized_bidders: { + 'bidder1': [0, 1, 2, 3, 4, 5, 6, 7, 10, 13, 15], + 'bidder2': [5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + } + } + + const WURFL = { + advertised_browser: 'Chrome', + advertised_browser_version: '125.0.6422.76', + advertised_device_os: 'Linux', + advertised_device_os_version: '6.5.0', + brand_name: 'Google', + complete_device_name: 'Google Chrome', + form_factor: 'Desktop', + is_app_webview: !1, + is_full_desktop: !0, + is_mobile: !1, + is_robot: !1, + is_smartphone: !1, + is_smarttv: !1, + is_tablet: !1, + manufacturer_name: '', + marketing_name: '', + } + + // expected analytics values + const expectedStatsURL = 'https://prebid.wurflcloud.com/v1/prebid/stats'; + const expectedData = JSON.stringify({ bidders: ['bidder1', 'bidder2'] }); + + let sandbox; + + beforeEach(function() { + sandbox = sinon.createSandbox(); + window.WURFLPromises = { + init: new Promise(function(resolve, reject) { resolve({ WURFL, wurfl_pbjs }) }), + complete: new Promise(function(resolve, reject) { resolve({ WURFL, wurfl_pbjs }) }), + }; + }); + + afterEach(() => { + // Restore the original functions + sandbox.restore(); + window.WURFLPromises = undefined; + }); + + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' }, + { bidder: 'bidder3' }, + ] + }], + ortb2Fragments: { + bidder: {}, + } + }; + + it('initialises the WURFL RTD provider', function () { + expect(wurflSubmodule.init()).to.be.true; + }); + + it('should enrich the bid request data', (done) => { + const expectedURL = new URL(altHost); + expectedURL.searchParams.set('debug', true); + expectedURL.searchParams.set('mode', 'prebid'); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({ + bidder1: { + device: { + ext: { + wurfl: { + advertised_browser: 'Chrome', + advertised_browser_version: '125.0.6422.76', + advertised_device_os: 'Linux', + advertised_device_os_version: '6.5.0', + brand_name: 'Google', + complete_device_name: 'Google Chrome', + form_factor: 'Desktop', + is_app_webview: !1, + is_robot: !1, + is_tablet: !1, + marketing_name: '', + }, + }, + }, + }, + bidder2: { + device: { + ext: { + wurfl: { + complete_device_name: 'Google Chrome', + form_factor: 'Desktop', + is_app_webview: !1, + is_full_desktop: !0, + is_mobile: !1, + is_robot: !1, + is_smartphone: !1, + is_smarttv: !1, + is_tablet: !1, + manufacturer_name: '', + }, + }, + }, + }, + bidder3: { + device: { + ext: { + wurfl: { + complete_device_name: 'Google Chrome', + form_factor: 'Desktop', + is_mobile: !1, + }, + }, + }, + }, + }); + done(); + }; + + const config = { + params: { + altHost: altHost, + debug: true, + } + }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + expect(loadExternalScriptStub.calledOnce).to.be.true; + const loadExternalScriptCall = loadExternalScriptStub.getCall(0); + expect(loadExternalScriptCall.args[0]).to.equal(expectedURL.toString()); + expect(loadExternalScriptCall.args[1]).to.equal('wurfl'); + }); + + it('onAuctionEndEvent: should send analytics data using navigator.sendBeacon, if available', () => { + const auctionDetails = {}; + const config = {}; + const userConsent = {}; + + const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon'); + + // Call the function + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Assertions + expect(sendBeaconStub.calledOnce).to.be.true; + expect(sendBeaconStub.calledWithExactly(expectedStatsURL, expectedData)).to.be.true; + }); + + it('onAuctionEndEvent: should send analytics data using fetch as fallback, if navigator.sendBeacon is not available', () => { + const auctionDetails = {}; + const config = {}; + const userConsent = {}; + + const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon').value(undefined); + const windowFetchStub = sandbox.stub(window, 'fetch'); + const fetchAjaxStub = sandbox.stub(ajaxModule, 'fetch'); + + // Call the function + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Assertions + expect(sendBeaconStub.called).to.be.false; + + expect(fetchAjaxStub.calledOnce).to.be.true; + const fetchAjaxCall = fetchAjaxStub.getCall(0); + expect(fetchAjaxCall.args[0]).to.equal(expectedStatsURL); + expect(fetchAjaxCall.args[1].method).to.equal('POST'); + expect(fetchAjaxCall.args[1].body).to.equal(expectedData); + expect(fetchAjaxCall.args[1].mode).to.equal('no-cors'); + }); + }); + + describe('bidderData', () => { + it('should return the WURFL data for a bidder', () => { + const wjsData = { + capability1: 'value1', + capability2: 'value2', + capability3: 'value3', + }; + const caps = ['capability1', 'capability2', 'capability3']; + const filter = [0, 2]; + + const result = bidderData(wjsData, caps, filter); + + expect(result).to.deep.equal({ + capability1: 'value1', + capability3: 'value3', + }); + }); + + it('should return an empty object if the filter is empty', () => { + const wjsData = { + capability1: 'value1', + capability2: 'value2', + capability3: 'value3', + }; + const caps = ['capability1', 'capability3']; + const filter = []; + + const result = bidderData(wjsData, caps, filter); + + expect(result).to.deep.equal({}); + }); + }); + + describe('lowEntropyData', () => { + it('should return the correct low entropy data for Apple devices', () => { + const wjsData = { + complete_device_name: 'Apple iPhone X', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const lowEntropyCaps = ['complete_device_name', 'form_factor', 'is_mobile']; + const expectedData = { + complete_device_name: 'Apple iPhone', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const result = lowEntropyData(wjsData, lowEntropyCaps); + expect(result).to.deep.equal(expectedData); + }); + + it('should return the correct low entropy data for Android devices', () => { + const wjsData = { + complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const lowEntropyCaps = ['complete_device_name', 'form_factor', 'is_mobile']; + const expectedData = { + complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const result = lowEntropyData(wjsData, lowEntropyCaps); + expect(result).to.deep.equal(expectedData); + }); + + it('should return an empty object if the lowEntropyCaps array is empty', () => { + const wjsData = { + complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', + form_factor: 'Smartphone', + is_mobile: !0, + }; + const lowEntropyCaps = []; + const expectedData = {}; + const result = lowEntropyData(wjsData, lowEntropyCaps); + expect(result).to.deep.equal(expectedData); + }); + }); + + describe('enrichBidderRequest', () => { + it('should enrich the bidder request with WURFL data', () => { + const reqBidsConfigObj = { + ortb2Fragments: { + bidder: { + exampleBidder: { + device: { + ua: 'user-agent', + } + } + } + } + }; + const bidderCode = 'exampleBidder'; + const wjsData = { + capability1: 'value1', + capability2: 'value2' + }; + + enrichBidderRequest(reqBidsConfigObj, bidderCode, wjsData); + + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({ + exampleBidder: { + device: { + ua: 'user-agent', + ext: { + wurfl: { + capability1: 'value1', + capability2: 'value2' + } + } + } + } + }); + }); + }); +}); From d6a58aeb12a0d9618c92a88634f3cf0c3884f46b Mon Sep 17 00:00:00 2001 From: tongwu-sh Date: Fri, 26 Jul 2024 09:35:14 +0800 Subject: [PATCH 09/51] ttd bid adapter: configurable endpoint (#12004) * support endpoint params for ttd adapter * update to support http2 endpoint only * remove dup code * add missing file * fix battr issue * fix warning * update connection type * add 5g --------- Co-authored-by: Tong Wu --- libraries/connectionInfo/connectionUtils.js | 33 +++++++++++++++ modules/prismaBidAdapter.js | 28 +------------ modules/ttdBidAdapter.js | 46 +++++++-------------- test/spec/modules/ttdBidAdapter_spec.js | 11 ++++- 4 files changed, 58 insertions(+), 60 deletions(-) create mode 100644 libraries/connectionInfo/connectionUtils.js diff --git a/libraries/connectionInfo/connectionUtils.js b/libraries/connectionInfo/connectionUtils.js new file mode 100644 index 00000000000..29fed27b91d --- /dev/null +++ b/libraries/connectionInfo/connectionUtils.js @@ -0,0 +1,33 @@ +/** + * Returns the type of connection. + * + * @returns {number} - Type of connection. + */ +export function getConnectionType() { + const connection = navigator.connection || navigator.webkitConnection; + if (!connection) { + return 0; + } + switch (connection.type) { + case 'ethernet': + return 1; + case 'wifi': + return 2; + case 'wimax': + return 6; + default: + switch (connection.effectiveType) { + case 'slow-2g': + case '2g': + return 4; + case '3g': + return 5; + case '4g': + return 6; + case '5g': + return 7; + default: + return connection.type == 'cellular' ? 3 : 0; + } + } +} diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index b42c4b8af3f..9f7d37dcebe 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -3,6 +3,7 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -20,33 +21,6 @@ const METRICS_TRACKER_URL = 'https://prisma.nexx360.io/track-imp'; const GVLID = 965; -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - default: - return 3; - } - default: - return 0; - } -} - export const spec = { code: BIDDER_CODE, gvlid: GVLID, diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js index d7705f2f5df..e9d0d3ca9f1 100644 --- a/modules/ttdBidAdapter.js +++ b/modules/ttdBidAdapter.js @@ -2,7 +2,8 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import {isNumber} from '../src/utils.js'; +import { isNumber } from '../src/utils.js'; +import { getConnectionType } from '../libraries/connectionInfo/connectionUtils.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -13,10 +14,11 @@ import {isNumber} from '../src/utils.js'; * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync */ -const BIDADAPTERVERSION = 'TTD-PREBID-2023.09.05'; +const BIDADAPTERVERSION = 'TTD-PREBID-2024.07.26'; const BIDDER_CODE = 'ttd'; const BIDDER_CODE_LONG = 'thetradedesk'; const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; +const BIDDER_ENDPOINT_HTTP2 = 'https://d2.adsrvr.org/bid/bidder/'; const USER_SYNC_ENDPOINT = 'https://match.adsrvr.org'; const MEDIA_TYPE = { @@ -99,33 +101,6 @@ function getDevice(firstPartyData) { return device; }; -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - default: - return 3; - } - default: - return 0; - } -} - function getUser(bidderRequest, firstPartyData) { let user = {}; if (bidderRequest.gdprConsent) { @@ -241,7 +216,7 @@ function banner(bid) { }, optionalParams); - const battr = utils.deepAccess(bid, 'ortb2Imp.battr'); + const battr = utils.deepAccess(bid, 'ortb2Imp.banner.battr'); if (battr) { banner.battr = battr; } @@ -318,7 +293,7 @@ function video(bid) { video.maxbitrate = maxbitrate; } - const battr = utils.deepAccess(bid, 'ortb2Imp.battr'); + const battr = utils.deepAccess(bid, 'ortb2Imp.video.battr'); if (battr) { video.battr = battr; } @@ -327,6 +302,13 @@ function video(bid) { } } +function selectEndpoint(params) { + if (params.useHttp2) { + return BIDDER_ENDPOINT_HTTP2; + } + return BIDDER_ENDPOINT; +} + export const spec = { code: BIDDER_CODE, gvlid: 21, @@ -443,7 +425,7 @@ export const spec = { topLevel.pmp = firstPartyData.pmp } - let url = BIDDER_ENDPOINT + bidderRequest.bids[0].params.supplySourceId; + let url = selectEndpoint(bidderRequest.bids[0].params) + bidderRequest.bids[0].params.supplySourceId; let serverRequest = { method: 'POST', diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 1fe504ba8e8..9f98a734d1c 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -277,6 +277,13 @@ describe('ttdBidAdapter', function () { expect(url).to.equal('https://direct.adsrvr.org/bid/bidder/supplier'); }); + it('sends bid requests to the correct custom endpoint', function () { + let bannerBidRequestsWithCustomEndpoint = deepClone(baseBannerBidRequests); + bannerBidRequestsWithCustomEndpoint[0].params.useHttp2 = true; + const url = testBuildRequests(bannerBidRequestsWithCustomEndpoint, baseBidderRequest).url; + expect(url).to.equal('https://d2.adsrvr.org/bid/bidder/supplier'); + }); + it('sends publisher id', function () { const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; expect(requestBody.site).to.be.not.null; @@ -442,7 +449,9 @@ describe('ttdBidAdapter', function () { let clonedBannerRequests = deepClone(baseBannerBidRequests); const battr = [1, 2, 3]; clonedBannerRequests[0].ortb2Imp = { - battr: battr + banner: { + battr: battr + } }; const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; expect(requestBody.imp[0].banner.battr).to.equal(battr); From 450f3a6b52cc3a8ac645f2c5acdd738cde2a8725 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Fri, 26 Jul 2024 13:18:46 +0000 Subject: [PATCH 10/51] Prebid 9.7.0 release --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ca36a169ae..1882e462e6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "9.7.0-pre", + "version": "9.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "9.7.0-pre", + "version": "9.7.0", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.24.6", diff --git a/package.json b/package.json index de8afe546ef..ddd87513771 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "9.7.0-pre", + "version": "9.7.0", "description": "Header Bidding Management Library", "main": "src/prebid.public.js", "exports": { From ac84d657721baff14b54f758e58dfeb557c857bc Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Fri, 26 Jul 2024 13:18:47 +0000 Subject: [PATCH 11/51] Increment version to 9.8.0-pre --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1882e462e6f..19a83a7b6ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "9.7.0", + "version": "9.8.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "9.7.0", + "version": "9.8.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.24.6", diff --git a/package.json b/package.json index ddd87513771..db93bbb4317 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "9.7.0", + "version": "9.8.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.public.js", "exports": { From d97968247b9e3c337bd1b26a280d5aebf49627ed Mon Sep 17 00:00:00 2001 From: Andrii Pukh <152202940+apukh-magnite@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:47:37 +0300 Subject: [PATCH 12/51] Rubicon Bid Adapter: fix hb_size undefined value for native media type (#12039) * Fix hb_size undefined value for native media type * Add unit test for native bids width and height --- modules/rubiconBidAdapter.js | 6 +++--- test/spec/modules/rubiconBidAdapter_spec.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 64bcdf78399..457c52d0750 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -219,9 +219,9 @@ export const converter = ortbConverter({ const {bidRequest} = context; let [parseSizeWidth, parseSizeHeight] = bidRequest.mediaTypes.video?.context === 'outstream' ? parseSizes(bidRequest, VIDEO) : [undefined, undefined]; - - bidResponse.width = bid.w || parseSizeWidth || bidResponse.playerWidth; - bidResponse.height = bid.h || parseSizeHeight || bidResponse.playerHeight; + // 0 by default to avoid undefined size + bidResponse.width = bid.w || parseSizeWidth || bidResponse.playerWidth || 0; + bidResponse.height = bid.h || parseSizeHeight || bidResponse.playerHeight || 0; if (bidResponse.mediaType === VIDEO && bidRequest.mediaTypes.video.context === 'outstream') { bidResponse.renderer = outstreamRenderer(bidResponse); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 9e25300e10b..a5d25da9123 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -3987,6 +3987,16 @@ describe('the rubicon adapter', function () { let bids = spec.interpretResponse({body: response}, {data: request}); expect(bids).to.have.nested.property('[0].native'); }); + it('should set 0 to bids width and height if `w` and `h` in response object not defined', () => { + const nativeBidderRequest = addNativeToBidRequest(bidderRequest); + const request = converter.toORTB({bidderRequest: nativeBidderRequest, bidRequests: nativeBidderRequest.bids}); + let response = getNativeResponse({impid: request.imp[0].id}); + delete response.seatbid[0].bid[0].w; + delete response.seatbid[0].bid[0].h + let bids = spec.interpretResponse({body: response}, {data: request}); + expect(bids[0].width).to.equal(0); + expect(bids[0].height).to.equal(0); + }) }); } From fa3f86aabd3ed70852f20d384b51b4366ad4d3b1 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Mon, 29 Jul 2024 17:25:38 -0400 Subject: [PATCH 13/51] Update omsBidAdapter.js (#12048) fixes https://github.com/prebid/Prebid.js/issues/12047 --- modules/omsBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index e6c8f8b098e..2fec1c1d7a9 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -136,7 +136,7 @@ function buildRequests(bidReqs, bidderRequest) { } function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || !bid.params || !bid.params.publisherId) { + if (!bid.params || !bid.params.publisherId) { return false; } From 295fc5e058958ec8919b52e67fb7dbe87f991304 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Tue, 30 Jul 2024 10:50:48 -0400 Subject: [PATCH 14/51] GitHub Actions: Update jscpd.yml (#12045) Eases up on the detection a bit --- .github/workflows/jscpd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml index 315fbb2ff09..de5f1408dff 100644 --- a/.github/workflows/jscpd.yml +++ b/.github/workflows/jscpd.yml @@ -29,7 +29,7 @@ jobs: run: | echo '{ "threshold": 20, - "minTokens": 50, + "minTokens": 100, "reporters": [ "json" ], From cd13fc40277c07d293855c5685843623015cdeb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:51:53 +0200 Subject: [PATCH 15/51] Rise Utils: Fix typo (#12058) --- libraries/riseUtils/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/riseUtils/index.js b/libraries/riseUtils/index.js index abd70d357e0..25c7183d552 100644 --- a/libraries/riseUtils/index.js +++ b/libraries/riseUtils/index.js @@ -324,7 +324,7 @@ export function generateGeneralParams(generalObject, bidderRequest, adapterVersi if (bidderRequest && bidderRequest.refererInfo) { generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); - generalParams.page_domain = deepAccess(bidderRequest, 'refererInfo.domain') || deepAccess(window, 'location.hostname'); + generalParams.site_domain = deepAccess(bidderRequest, 'refererInfo.domain') || deepAccess(window, 'location.hostname'); } return generalParams; From a103a2e1df4e1c0568c5cf6994b15b7e49bdd08d Mon Sep 17 00:00:00 2001 From: olafbuitelaar Date: Tue, 30 Jul 2024 21:04:15 +0200 Subject: [PATCH 16/51] CORE: prevent unbound growth of suspendedTimeouts and possible NaN values (#12059) * prevent unbound growth of suspendedTimeouts and possible NaN values in timeOutOfFocus * Add tests * Reintroduce outOfFocusStart default to 0 --------- Co-authored-by: Demetrio Girardi --- src/utils/focusTimeout.js | 23 +++++++++---- src/utils/ttlCollection.js | 2 +- test/spec/unit/utils/focusTimeout_spec.js | 39 +++++++++++++++++++---- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/utils/focusTimeout.js b/src/utils/focusTimeout.js index 0ba66cc4efc..0c54bacec97 100644 --- a/src/utils/focusTimeout.js +++ b/src/utils/focusTimeout.js @@ -1,16 +1,27 @@ -let outOfFocusStart; +let outOfFocusStart = null; // enforce null otherwise it could be undefined and the callback wouldn't execute let timeOutOfFocus = 0; let suspendedTimeouts = []; -document.addEventListener('visibilitychange', () => { +function trackTimeOutOfFocus() { if (document.hidden) { outOfFocusStart = Date.now() } else { - timeOutOfFocus += Date.now() - outOfFocusStart - suspendedTimeouts.forEach(({ callback, startTime, setTimerId }) => setTimerId(setFocusTimeout(callback, timeOutOfFocus - startTime)())) + timeOutOfFocus += Date.now() - (outOfFocusStart ?? 0); // when the page is loaded in hidden state outOfFocusStart is undefined, which results in timeoutOffset being NaN outOfFocusStart = null; + suspendedTimeouts.forEach(({ callback, startTime, setTimerId }) => setTimerId(setFocusTimeout(callback, timeOutOfFocus - startTime)())); + suspendedTimeouts = []; } -}); +} + +document.addEventListener('visibilitychange', trackTimeOutOfFocus); + +export function reset() { + outOfFocusStart = null; + timeOutOfFocus = 0; + suspendedTimeouts = []; + document.removeEventListener('visibilitychange', trackTimeOutOfFocus); + document.addEventListener('visibilitychange', trackTimeOutOfFocus); +} /** * Wraps native setTimeout function in order to count time only when page is focused @@ -19,7 +30,7 @@ document.addEventListener('visibilitychange', () => { * @param {number} [milliseconds] - Minimum duration (in milliseconds) that the callback will be executed after * @returns {function(*): (number)} - Getter function for current timer id */ -export default function setFocusTimeout(callback, milliseconds) { +export function setFocusTimeout(callback, milliseconds) { const startTime = timeOutOfFocus; let timerId = setTimeout(() => { if (timeOutOfFocus === startTime && outOfFocusStart == null) { diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js index b6e0a5198df..ffc11433d06 100644 --- a/src/utils/ttlCollection.js +++ b/src/utils/ttlCollection.js @@ -1,6 +1,6 @@ import {GreedyPromise} from './promise.js'; import {binarySearch, logError, timestamp} from '../utils.js'; -import setFocusTimeout from './focusTimeout.js'; +import {setFocusTimeout} from './focusTimeout.js'; /** * Create a set-like collection that automatically forgets items after a certain time. diff --git a/test/spec/unit/utils/focusTimeout_spec.js b/test/spec/unit/utils/focusTimeout_spec.js index ed7b1c0c2f3..3fcc4af18fe 100644 --- a/test/spec/unit/utils/focusTimeout_spec.js +++ b/test/spec/unit/utils/focusTimeout_spec.js @@ -1,4 +1,4 @@ -import setFocusTimeout from '../../../../src/utils/focusTimeout'; +import {setFocusTimeout, reset} from '../../../../src/utils/focusTimeout'; export const setDocumentHidden = (hidden) => { Object.defineProperty(document, 'hidden', { @@ -9,10 +9,12 @@ export const setDocumentHidden = (hidden) => { }; describe('focusTimeout', () => { - let clock; + let clock, callback; beforeEach(() => { + reset() clock = sinon.useFakeTimers(); + callback = sinon.spy(); }); afterEach(() => { @@ -20,14 +22,21 @@ describe('focusTimeout', () => { }) it('should invoke callback when page is visible', () => { - let callback = sinon.stub(); setFocusTimeout(callback, 2000); clock.tick(2000); expect(callback.called).to.be.true; }); + it('should not choke if page starts hidden', () => { + setDocumentHidden(true); + reset(); + setDocumentHidden(false); + setFocusTimeout(callback, 1000); + clock.tick(1000); + sinon.assert.called(callback); + }) + it('should not invoke callback if page was hidden', () => { - let callback = sinon.stub(); setFocusTimeout(callback, 2000); setDocumentHidden(true); clock.tick(3000); @@ -35,7 +44,6 @@ describe('focusTimeout', () => { }); it('should defer callback execution when page is hidden', () => { - let callback = sinon.stub(); setFocusTimeout(callback, 4000); clock.tick(2000); setDocumentHidden(true); @@ -46,8 +54,27 @@ describe('focusTimeout', () => { expect(callback.called).to.be.true; }); + it('should not execute deferred callbacks again', () => { + setDocumentHidden(true); + setFocusTimeout(callback, 1000); + clock.tick(2000); + [false, true, false].forEach(setDocumentHidden); + clock.tick(2000); + sinon.assert.calledOnce(callback); + }); + + it('should run callbacks that expire while page is hidden', () => { + setFocusTimeout(callback, 1000); + clock.tick(500); + setDocumentHidden(true); + clock.tick(1000); + setDocumentHidden(false); + sinon.assert.notCalled(callback); + clock.tick(1000); + sinon.assert.called(callback); + }) + it('should return updated timerId after page was showed again', () => { - let callback = sinon.stub(); const getCurrentTimerId = setFocusTimeout(callback, 4000); const oldTimerId = getCurrentTimerId(); clock.tick(2000); From d7f69fe414718175ebac29455899c974e014c94b Mon Sep 17 00:00:00 2001 From: Daria Boyko Date: Wed, 31 Jul 2024 14:11:33 +0300 Subject: [PATCH 17/51] admixerBidAdapter: fix bid floor (#12062) * Update README.md update * Add admixerwl alias for admixerBidAdapter. * fix bid floor on admixerBidAdapter * add spaces --------- Co-authored-by: AdmixerTech <35560933+AdmixerTech@users.noreply.github.com> Co-authored-by: AdmixerTech Co-authored-by: Yaroslav Masenko Co-authored-by: Daria Boyko --- modules/admixerBidAdapter.js | 17 ++++++++++++----- test/spec/modules/admixerBidAdapter_spec.js | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index e979bd07b51..4deeaee5206 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -1,4 +1,4 @@ -import {isStr, logError} from '../src/utils.js'; +import {isStr, logError, isFn, deepAccess} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; @@ -71,15 +71,15 @@ export const spec = { if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } - let bidFloor = getBidFloor(bidderRequest); - if (bidFloor) { - payload.bidFloor = bidFloor; - } } validRequest.forEach((bid) => { let imp = {}; Object.keys(bid).forEach(key => imp[key] = bid[key]); imp.ortb2 && delete imp.ortb2; + let bidFloor = getBidFloor(bid); + if (bidFloor) { + imp.bidFloor = bidFloor; + } payload.imps.push(imp); }); @@ -117,10 +117,16 @@ export const spec = { return pixels; } }; + function getEndpointUrl(code) { return find(ALIASES, (val) => val.code === code)?.endpoint || ENDPOINT_URL; } + function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + try { const bidFloor = bid.getFloor({ currency: 'USD', @@ -132,4 +138,5 @@ function getBidFloor(bid) { return 0; } } + registerBidder(spec); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index e53457e03c4..1da6a58bea3 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -226,12 +226,12 @@ describe('AdmixerAdapter', function () { }, }; it('gets floor', function () { - bidderRequest.getFloor = () => { + validRequest[0].getFloor = () => { return { floor: 0.6 }; }; const request = spec.buildRequests(validRequest, bidderRequest); const payload = request.data; - expect(payload.bidFloor).to.deep.equal(0.6); + expect(payload.imps[0].bidFloor).to.deep.equal(0.6); }); }); From aa4d4307dadc1e8a6a8934a022630de3a6066f09 Mon Sep 17 00:00:00 2001 From: sebastienrufiange <131205907+sebastienrufiange@users.noreply.github.com> Date: Wed, 31 Jul 2024 07:25:30 -0400 Subject: [PATCH 18/51] Contxtful RTD Provider: Pass module config (#12034) * feat: pass prebid config * doc: config * fix: better event registration * fix: tagId check --- modules/contxtfulRtdProvider.js | 27 +++++++------ .../spec/modules/contxtfulRtdProvider_spec.js | 38 +++++++++---------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/modules/contxtfulRtdProvider.js b/modules/contxtfulRtdProvider.js index 03050a6a64f..d4bcd94ff4a 100644 --- a/modules/contxtfulRtdProvider.js +++ b/modules/contxtfulRtdProvider.js @@ -101,8 +101,7 @@ function init(config) { rxApi = null; try { - const { version, customer, hostname } = extractParameters(config); - initCustomer(version, customer, hostname); + initCustomer(config); return true; } catch (error) { logError(MODULE, error); @@ -137,35 +136,39 @@ export function extractParameters(config) { /** * 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 + * @param { String } config */ -function initCustomer(version, customer, hostname) { +function initCustomer(config) { + const { version, customer, hostname } = extractParameters(config); const CONNECTOR_URL = buildUrl({ protocol: 'https', host: hostname, pathname: `/${version}/prebid/${customer}/connector/rxConnector.js`, }); - const externalScript = loadExternalScript(CONNECTOR_URL, MODULE_NAME); - addExternalScriptEventListener(externalScript, customer); + addConnectorEventListener(customer, config); + loadExternalScript(CONNECTOR_URL, MODULE_NAME); } /** * Add event listener to the script tag for the expected events from the external script. - * @param { HTMLScriptElement } script + * @param { String } tagId + * @param { String } prebidConfig */ -function addExternalScriptEventListener(script, tagId) { - script.addEventListener( +function addConnectorEventListener(tagId, prebidConfig) { + window.addEventListener( 'rxConnectorIsReady', - async ({ detail: rxConnector }) => { + async ({ detail: { [tagId]: rxConnector } }) => { + if (!rxConnector) { + return; + } // Fetch the customer configuration const { rxApiBuilder, fetchConfig } = rxConnector; let config = await fetchConfig(tagId); if (!config) { return; } + config['prebid'] = prebidConfig || {}; rxApi = await rxApiBuilder(config); } ); diff --git a/test/spec/modules/contxtfulRtdProvider_spec.js b/test/spec/modules/contxtfulRtdProvider_spec.js index 5dda23caafc..ad79b051393 100644 --- a/test/spec/modules/contxtfulRtdProvider_spec.js +++ b/test/spec/modules/contxtfulRtdProvider_spec.js @@ -17,7 +17,7 @@ const RX_CONNECTOR_MOCK = { }; const TIMEOUT = 10; -const RX_CONNECTOR_IS_READY_EVENT = new CustomEvent('rxConnectorIsReady', { detail: RX_CONNECTOR_MOCK }); +const RX_CONNECTOR_IS_READY_EVENT = new CustomEvent('rxConnectorIsReady', { detail: {[CUSTOMER]: RX_CONNECTOR_MOCK}, bubbles: true }); function writeToStorage(requester, timeDiff) { let rx = RX_FROM_SESSION_STORAGE; @@ -157,30 +157,30 @@ describe('contxtfulRtdProvider', function () { }); describe('init', function () { - it('gets the RX API returned by an external script', (done) => { + it('uses the RX API to get receptivity', (done) => { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); setTimeout(() => { contxtfulSubmodule.getTargetingData(['ad-slot'], config); - expect(RX_CONNECTOR_MOCK.fetchConfig.callCount, 'fetchConfig').to.be.equal(1); - expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount, 'rxApiBuilder').to.be.equal(1); + expect(RX_API_MOCK.receptivity.callCount, 'receptivity 42').to.be.equal(1); + expect(RX_API_MOCK.receptivity.firstCall.returnValue, 'receptivity').to.be.equal(RX_FROM_API); done(); }, TIMEOUT); }); }); describe('init', function () { - it('uses the RX API to get receptivity', (done) => { + it('gets the RX API returned by an external script', (done) => { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); setTimeout(() => { contxtfulSubmodule.getTargetingData(['ad-slot'], config); - expect(RX_API_MOCK.receptivity.callCount, 'receptivity 42').to.be.equal(1); - expect(RX_API_MOCK.receptivity.firstCall.returnValue, 'receptivity').to.be.equal(RX_FROM_API); + expect(RX_CONNECTOR_MOCK.fetchConfig.callCount, 'fetchConfig').at.least(1); + expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount, 'rxApiBuilder').at.least(1); done(); }, TIMEOUT); }); @@ -245,7 +245,7 @@ describe('contxtfulRtdProvider', function () { it('adds receptivity to the ad units using the RX API', function (done) { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); setTimeout(() => { let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); @@ -278,7 +278,7 @@ describe('contxtfulRtdProvider', function () { let config = buildInitConfig(VERSION, CUSTOMER); config.params.adServerTargeting = false; contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); setTimeout(() => { let _ = contxtfulSubmodule.getTargetingData(adUnits, config); @@ -291,7 +291,7 @@ describe('contxtfulRtdProvider', function () { let config = buildInitConfig(VERSION, CUSTOMER); config.params.adServerTargeting = false; contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); setTimeout(() => { let targetingData = contxtfulSubmodule.getTargetingData(adUnits, config); @@ -375,7 +375,7 @@ describe('contxtfulRtdProvider', function () { describe('getBidRequestData', function () { it('calls once the onDone callback', function (done) { contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); let reqBidsConfigObj = { ortb2Fragments: { @@ -397,7 +397,7 @@ describe('contxtfulRtdProvider', function () { it('does not write receptivity to the global OpenRTB 2 fragment', function (done) { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); let reqBidsConfigObj = { ortb2Fragments: { @@ -419,7 +419,7 @@ describe('contxtfulRtdProvider', function () { it('writes receptivity to the configured bidder OpenRTB 2 fragments', function (done) { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); let reqBidsConfigObj = { ortb2Fragments: { @@ -505,7 +505,7 @@ describe('contxtfulRtdProvider', function () { it('uses the RX API', function (done) { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); let reqBidsConfigObj = { ortb2Fragments: { @@ -515,8 +515,8 @@ describe('contxtfulRtdProvider', function () { }; setTimeout(() => { - expect(RX_CONNECTOR_MOCK.fetchConfig.callCount).to.equal(1); - expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount).to.equal(1); + expect(RX_CONNECTOR_MOCK.fetchConfig.callCount).at.least(1); + expect(RX_CONNECTOR_MOCK.rxApiBuilder.callCount).at.least(1); const onDoneSpy = sinon.spy(); contxtfulSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, config); expect(onDoneSpy.callCount).to.equal(1); @@ -530,7 +530,7 @@ describe('contxtfulRtdProvider', function () { it('adds receptivity to the reqBidsConfigObj', function (done) { let config = buildInitConfig(VERSION, CUSTOMER); contxtfulSubmodule.init(config); - loadExternalScriptTag.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); + window.dispatchEvent(RX_CONNECTOR_IS_READY_EVENT); let reqBidsConfigObj = { ortb2Fragments: { From b98353b46ba291c97d443f655c8307fef7c16fc5 Mon Sep 17 00:00:00 2001 From: Sajid Mahmood Date: Wed, 31 Jul 2024 07:33:02 -0400 Subject: [PATCH 19/51] IX Bid Adapter: propagate atype in uids (#12050) Co-authored-by: Sajid Mahmood --- modules/ixBidAdapter.js | 1 - test/spec/modules/ixBidAdapter_spec.js | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index e182634b52a..776af4ab067 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -659,7 +659,6 @@ function getEidInfo(allEids) { rtiPartner: SOURCE_RTI_MAPPING[eid.source] }; } - delete eid.uids[0].atype; toSend.push(eid); if (toSend.length >= MAX_EID_SOURCES) { break; diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 42c0c2afdf5..47583031982 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -856,7 +856,8 @@ describe('IndexexchangeAdapter', function () { source: 'identityinc.com', uids: [ { - id: 'identityid' + id: 'identityid', + atype: 1 } ] } @@ -1383,8 +1384,14 @@ describe('IndexexchangeAdapter', function () { it('identity data in impression should have correct format and value (single identity partner)', function () { const impression = payload.user.eids; + expect(impression).to.be.an('array'); + expect(impression).to.have.lengthOf(1); expect(impression[0].source).to.equal(testCopy.IdentityIp.data.source); + expect(impression[0].uids).to.be.an('array'); + expect(impression[0].uids).to.have.lengthOf(1); expect(impression[0].uids[0].id).to.equal(testCopy.IdentityIp.data.uids[0].id); + expect(impression[0].uids[0].atype).to.exist; + expect(impression[0].uids[0].atype).to.equal(testCopy.IdentityIp.data.uids[0].atype); }); }); From 7289e2bad24045b5c63176fc78266467adb5ceaa Mon Sep 17 00:00:00 2001 From: Phaneendra Hegde Date: Wed, 31 Jul 2024 19:12:19 +0530 Subject: [PATCH 20/51] Pubxai Analytics Adapter: add additional event listener to collect bidRejected data (#12063) * send BidRejected Events to capture floored bids * fix tests * send pubx_id as query param --------- Co-authored-by: tej656 --- modules/pubxaiAnalyticsAdapter.js | 13 +++++++++---- test/spec/modules/pubxaiAnalyticsAdapter_spec.js | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index afa8b01bfca..f8ec72cde75 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -157,15 +157,19 @@ const track = ({ eventType, args }) => { // handle invalid bids, and remove them from the adUnit cache case EVENTS.BID_TIMEOUT: args.map(extractBid).forEach((bid) => { - bid.renderStatus = 3; + bid.bidType = 3; auctionCache[bid.auctionId].bids.push(bid); }); break; // handle valid bid responses and record them as part of an auction case EVENTS.BID_RESPONSE: - const bid = Object.assign(extractBid(args), { renderStatus: 2 }); + const bid = Object.assign(extractBid(args), { bidType: 2 }); auctionCache[bid.auctionId].bids.push(bid); break; + case EVENTS.BID_REJECTED: + const rejectedBid = Object.assign(extractBid(args), { bidType: 1 }); + auctionCache[rejectedBid.auctionId].bids.push(rejectedBid); + break; // capture extra information from the auction, and if there were no bids // (and so no chance of a win) send the auction case EVENTS.AUCTION_END: @@ -183,7 +187,7 @@ const track = ({ eventType, args }) => { timestamp: args.timestamp, }); if ( - auctionCache[args.auctionId].bids.every((bid) => bid.renderStatus === 3) + auctionCache[args.auctionId].bids.every((bid) => bid.bidType === 3) ) { prepareSend(args.auctionId); } @@ -201,7 +205,7 @@ const track = ({ eventType, args }) => { isFloorSkipped: floorDetail?.skipped || false, isWinningBid: true, renderedSize: args.size, - renderStatus: 4, + bidType: 4, }); winningBid.adServerData = getAdServerDataForBid(winningBid); auctionCache[winningBid.auctionId].winningBid = winningBid; @@ -283,6 +287,7 @@ const prepareSend = (auctionId) => { auctionTimestamp: auctionData.auctionDetail.timestamp, pubxaiAnalyticsVersion: pubxaiAnalyticsVersion, prebidVersion: '$prebid.version$', + pubxId: initOptions.pubxId, }, }); sendCache[pubxaiAnalyticsRequestUrl].push(data); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 237a2d32d54..abc52b00439 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -31,9 +31,10 @@ describe('pubxai analytics adapter', () => { }); describe('track', () => { + const pubxId = '6c415fc0-8b0e-4cf5-be73-01526a4db625'; let initOptions = { samplingRate: '1', - pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625', + pubxId: pubxId, }; let originalVS; @@ -148,7 +149,7 @@ describe('pubxai analytics adapter', () => { timeout: 1000, config: { samplingRate: '1', - pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625', + pubxId: pubxId, }, }, bidRequested: { @@ -529,7 +530,7 @@ describe('pubxai analytics adapter', () => { null, auctionId: 'bc3806e4-873e-453c-8ae5-204f35e923b4', sizes: '300x250', - renderStatus: 2, + bidType: 2, requestTimestamp: 1616654312804, creativeId: 96846035, currency: 'USD', @@ -651,7 +652,7 @@ describe('pubxai analytics adapter', () => { placementId: 13144370, renderedSize: '300x250', sizes: '300x250', - renderStatus: 4, + bidType: 4, responseTimestamp: 1616654313071, requestTimestamp: 1616654312804, status: 'rendered', @@ -764,6 +765,7 @@ describe('pubxai analytics adapter', () => { auctionTimestamp: '1616654312804', pubxaiAnalyticsVersion: 'v2.0.0', prebidVersion: '$prebid.version$', + pubxId: pubxId, }); expect(expectedData.type).to.equal('text/json'); expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ @@ -807,6 +809,7 @@ describe('pubxai analytics adapter', () => { auctionTimestamp: '1616654312804', pubxaiAnalyticsVersion: 'v2.0.0', prebidVersion: '$prebid.version$', + pubxId: pubxId, }); // Step 9: check that the data sent in the request is correct @@ -932,6 +935,7 @@ describe('pubxai analytics adapter', () => { auctionTimestamp: '1616654312804', pubxaiAnalyticsVersion: 'v2.0.0', prebidVersion: '$prebid.version$', + pubxId: pubxId, }); expect(expectedData.type).to.equal('text/json'); expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ @@ -1048,6 +1052,7 @@ describe('pubxai analytics adapter', () => { auctionTimestamp: '1616654312804', pubxaiAnalyticsVersion: 'v2.0.0', prebidVersion: '$prebid.version$', + pubxId: pubxId, }); expect(expectedData.type).to.equal('text/json'); expect(JSON.parse(await readBlobSafariCompat(expectedData))).to.deep.equal([ From be3744030296b9b5caedb94ce1904e8f242280cf Mon Sep 17 00:00:00 2001 From: Oleksandr Yermakov <47504677+sanychtasher@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:34:22 +0300 Subject: [PATCH 21/51] mgid bid adapters: refactoring for trimmer code (#12057) * added consts for values * added copy method * moved extractDomainFromHost * added method triggerNurlWithCpm * added getUserSyncs * less formatting mgidBid * less formatting undertoneBid * less formatting utils * renamed adm to nativeAdm * back changes for other adaptes * back changes * moved getUserSyncs to lib pkg * less changes * fixed errors * less changes * fixed errors * fixed tests --------- Co-authored-by: Oleksandr Yermakov --- libraries/mgidUtils/mgidUtils.js | 73 ++++++++++++++++ modules/mgidBidAdapter.js | 137 +++++++------------------------ modules/mgidXBidAdapter.js | 68 +-------------- modules/redtramBidAdapter.js | 8 +- modules/undertoneBidAdapter.js | 20 +---- src/utils.js | 28 +++++++ 6 files changed, 135 insertions(+), 199 deletions(-) create mode 100644 libraries/mgidUtils/mgidUtils.js diff --git a/libraries/mgidUtils/mgidUtils.js b/libraries/mgidUtils/mgidUtils.js new file mode 100644 index 00000000000..9ac84e231b7 --- /dev/null +++ b/libraries/mgidUtils/mgidUtils.js @@ -0,0 +1,73 @@ +import { + isPlainObject, + isArray, + isStr, + isNumber, +} from '../../src/utils.js'; +import { config } from '../../src/config.js'; +import { USERSYNC_DEFAULT_CONFIG } from '../../src/userSync.js'; + +const PIXEL_SYNC_URL = 'https://cm.mgid.com/i.gif'; +const IFRAME_SYNC_URL = 'https://cm.mgid.com/i.html'; + +export function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + const spb = isPlainObject(config.getConfig('userSync')) && + isNumber(config.getConfig('userSync').syncsPerBidder) + ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; + + if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + let pixels = []; + if (serverResponses && + isArray(serverResponses) && + serverResponses.length > 0 && + isPlainObject(serverResponses[0].body) && + isPlainObject(serverResponses[0].body.ext) && + isArray(serverResponses[0].body.ext.cm) && + serverResponses[0].body.ext.cm.length > 0) { + pixels = serverResponses[0].body.ext.cm; + } + + const syncs = []; + const query = []; + query.push('cbuster={cbuster}'); + query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); + if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { + query.push('gdpr=1'); + } else { + query.push('gdpr=0'); + } + if (isPlainObject(uspConsent) && uspConsent?.consentString) { + query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); + } + if (isPlainObject(gppConsent) && gppConsent?.gppString) { + query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); + } + if (config.getConfig('coppa')) { + query.push('coppa=1') + } + const q = query.join('&') + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: IFRAME_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) + }); + } else if (syncOptions.pixelEnabled) { + if (pixels.length === 0) { + for (let i = 0; i < spb; i++) { + syncs.push({ + type: 'image', + url: PIXEL_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required + }); + } + } else { + for (let i = 0; i < spb && i < pixels.length; i++) { + syncs.push({ + type: 'image', + url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) + }); + } + } + } + return syncs; + } +} diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 5cfef325d2f..70a9290aa4d 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -8,10 +8,12 @@ import { parseUrl, isEmpty, triggerPixel, + triggerNurlWithCpm, logWarn, isFn, isNumber, isBoolean, + extractDomainFromHost, isInteger, deepSetValue, getBidIdParameter, setOnAny } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -19,7 +21,7 @@ import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {USERSYNC_DEFAULT_CONFIG} from '../src/userSync.js'; +import { getUserSyncs } from '../libraries/mgidUtils/mgidUtils.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -231,7 +233,7 @@ export const spec = { const page = deepAccess(bidderRequest, 'refererInfo.page') || info.location if (!isStr(deepAccess(request.site, 'domain'))) { const hostname = parseUrl(page).hostname; - request.site.domain = extractDomainFromHost(hostname) || hostname + request.site.domain = extractDomainFromHostExceptLocalhost(hostname) || hostname } if (!isStr(deepAccess(request.site, 'page'))) { request.site.page = page @@ -344,13 +346,7 @@ export const spec = { }, 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); - } + triggerNurlWithCpm(bid, cpm) if (bid.isBurl) { if (bid.mediaType === BANNER) { bid.ad = bid.ad.replace( @@ -367,68 +363,7 @@ export const spec = { } logInfo(LOG_INFO_PREFIX + `onBidWon`); }, - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - logInfo(LOG_INFO_PREFIX + `getUserSyncs`); - const spb = isPlainObject(config.getConfig('userSync')) && - isNumber(config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; - - if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { - let pixels = []; - if (serverResponses && - isArray(serverResponses) && - serverResponses.length > 0 && - isPlainObject(serverResponses[0].body) && - isPlainObject(serverResponses[0].body.ext) && - isArray(serverResponses[0].body.ext.cm) && - serverResponses[0].body.ext.cm.length > 0) { - pixels = serverResponses[0].body.ext.cm; - } - - const syncs = []; - const query = []; - query.push('cbuster={cbuster}'); - query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); - if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { - query.push('gdpr=1'); - } else { - query.push('gdpr=0'); - } - if (isPlainObject(uspConsent) && uspConsent?.consentString) { - query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); - } - if (isPlainObject(gppConsent) && gppConsent?.gppString) { - query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); - } - if (config.getConfig('coppa')) { - query.push('coppa=1') - } - const q = query.join('&') - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: 'https://cm.mgid.com/i.html?' + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } else if (syncOptions.pixelEnabled) { - if (pixels.length === 0) { - for (let i = 0; i < spb; i++) { - syncs.push({ - type: 'image', - url: 'https://cm.mgid.com/i.gif?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required - }); - } - } else { - for (let i = 0; i < spb && i < pixels.length; i++) { - syncs.push({ - type: 'image', - url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } - } - } - return syncs; - } - } + getUserSyncs: getUserSyncs, }; registerBidder(spec); @@ -479,25 +414,11 @@ function setMediaType(bid, newBid) { } } -function extractDomainFromHost(pageHost) { +function extractDomainFromHostExceptLocalhost(pageHost) { if (pageHost === 'localhost') { return 'localhost' } - let domain = null; - try { - let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); - if (domains != null && domains.length > 0) { - domain = domains[0]; - for (let i = 1; i < domains.length; i++) { - if (domains[i].length > domain.length) { - domain = domains[i]; - } - } - } - } catch (e) { - domain = null; - } - return domain; + return extractDomainFromHost(pageHost) } function getLanguage() { @@ -675,33 +596,25 @@ function commonNativeRequestObject(nativeAsset, params) { function parseNativeResponse(bid, newBid) { newBid.native = {}; if (bid.hasOwnProperty('adm')) { - let adm = ''; + let nativeAdm = ''; try { - adm = JSON.parse(bid.adm); + nativeAdm = JSON.parse(bid.adm); } catch (ex) { logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native response for ad response: ' + newBid.adm); return; } - if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) { + if (nativeAdm && nativeAdm.native && nativeAdm.native.assets && nativeAdm.native.assets.length > 0) { newBid.mediaType = NATIVE; - for (let i = 0, len = adm.native.assets.length; i < len; i++) { - switch (adm.native.assets[i].id) { + for (let i = 0, len = nativeAdm.native.assets.length; i < len; i++) { + switch (nativeAdm.native.assets[i].id) { case NATIVE_ASSETS.TITLE.ID: - newBid.native.title = adm.native.assets[i].title && adm.native.assets[i].title.text; + newBid.native.title = nativeAdm.native.assets[i].title && nativeAdm.native.assets[i].title.text; break; case NATIVE_ASSETS.IMAGE.ID: - newBid.native.image = { - url: adm.native.assets[i].img && adm.native.assets[i].img.url, - height: adm.native.assets[i].img && adm.native.assets[i].img.h, - width: adm.native.assets[i].img && adm.native.assets[i].img.w, - }; + newBid.native.image = copyFromAdmAsset(nativeAdm.native.assets[i]); break; case NATIVE_ASSETS.ICON.ID: - newBid.native.icon = { - url: adm.native.assets[i].img && adm.native.assets[i].img.url, - height: adm.native.assets[i].img && adm.native.assets[i].img.h, - width: adm.native.assets[i].img && adm.native.assets[i].img.w, - }; + newBid.native.icon = copyFromAdmAsset(nativeAdm.native.assets[i]); break; case NATIVE_ASSETS.SPONSOREDBY.ID: case NATIVE_ASSETS.SPONSORED.ID: @@ -711,14 +624,14 @@ function parseNativeResponse(bid, newBid) { case NATIVE_ASSETS.BODY.ID: case NATIVE_ASSETS.DISPLAYURL.ID: case NATIVE_ASSETS.CTA.ID: - newBid.native[spec.NATIVE_ASSET_ID_TO_KEY_MAP[adm.native.assets[i].id]] = adm.native.assets[i].data && adm.native.assets[i].data.value; + newBid.native[spec.NATIVE_ASSET_ID_TO_KEY_MAP[nativeAdm.native.assets[i].id]] = nativeAdm.native.assets[i].data && nativeAdm.native.assets[i].data.value; break; } } - newBid.native.clickUrl = adm.native.link && adm.native.link.url; - newBid.native.clickTrackers = (adm.native.link && adm.native.link.clicktrackers) || []; - newBid.native.impressionTrackers = adm.native.imptrackers || []; - newBid.native.jstracker = adm.native.jstracker || []; + newBid.native.clickUrl = nativeAdm.native.link && nativeAdm.native.link.url; + newBid.native.clickTrackers = (nativeAdm.native.link && nativeAdm.native.link.clicktrackers) || []; + newBid.native.impressionTrackers = nativeAdm.native.imptrackers || []; + newBid.native.jstracker = nativeAdm.native.jstracker || []; newBid.width = 0; newBid.height = 0; } @@ -778,3 +691,11 @@ function getBidFloor(bid, cur) { } return {floor: bidFloor, cur: cur} } + +function copyFromAdmAsset(asset) { + return { + url: asset.img && asset.img.url, + height: asset.img && asset.img.h, + width: asset.img && asset.img.w, + } +} diff --git a/modules/mgidXBidAdapter.js b/modules/mgidXBidAdapter.js index 471e8fb2754..48648389bcf 100644 --- a/modules/mgidXBidAdapter.js +++ b/modules/mgidXBidAdapter.js @@ -1,15 +1,11 @@ -import { isPlainObject, isNumber, isArray, isStr } 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 { USERSYNC_DEFAULT_CONFIG } from '../src/userSync.js'; import { isBidRequestValid, buildRequestsBase, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; +import { getUserSyncs } from '../libraries/mgidUtils/mgidUtils.js' const BIDDER_CODE = 'mgidX'; const GVLID = 358; const AD_URL = 'https://#{REGION}#.mgid.com/pbjs'; -const PIXEL_SYNC_URL = 'https://cm.mgid.com/i.gif'; -const IFRAME_SYNC_URL = 'https://cm.mgid.com/i.html'; const buildRequests = (validBidRequests = [], bidderRequest = {}) => { const request = buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest }); @@ -33,67 +29,7 @@ export const spec = { buildRequests, interpretResponse, - getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { - const spb = isPlainObject(config.getConfig('userSync')) && - isNumber(config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; - - if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { - let pixels = []; - if (serverResponses && - isArray(serverResponses) && - serverResponses.length > 0 && - isPlainObject(serverResponses[0].body) && - isPlainObject(serverResponses[0].body.ext) && - isArray(serverResponses[0].body.ext.cm) && - serverResponses[0].body.ext.cm.length > 0) { - pixels = serverResponses[0].body.ext.cm; - } - - const syncs = []; - const query = []; - query.push('cbuster={cbuster}'); - query.push('gdpr_consent=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); - if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { - query.push('gdpr=1'); - } else { - query.push('gdpr=0'); - } - if (isPlainObject(uspConsent) && uspConsent?.consentString) { - query.push(`us_privacy=${encodeURIComponent(uspConsent?.consentString)}`); - } - if (isPlainObject(gppConsent) && gppConsent?.gppString) { - query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); - } - if (config.getConfig('coppa')) { - query.push('coppa=1') - } - const q = query.join('&') - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: IFRAME_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } else if (syncOptions.pixelEnabled) { - if (pixels.length === 0) { - for (let i = 0; i < spb; i++) { - syncs.push({ - type: 'image', - url: PIXEL_SYNC_URL + '?' + q.replace('{cbuster}', Math.round(new Date().getTime())) // randomly selects partner if sync required - }); - } - } else { - for (let i = 0; i < spb && i < pixels.length; i++) { - syncs.push({ - type: 'image', - url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + q.replace('{cbuster}', Math.round(new Date().getTime())) - }); - } - } - } - return syncs; - } - } + getUserSyncs: getUserSyncs, }; registerBidder(spec); diff --git a/modules/redtramBidAdapter.js b/modules/redtramBidAdapter.js index e1dc0e2a148..726b2d53f1c 100644 --- a/modules/redtramBidAdapter.js +++ b/modules/redtramBidAdapter.js @@ -1,9 +1,8 @@ import { isFn, - isStr, deepAccess, getWindowTop, - triggerPixel + triggerNurlWithCpm } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; @@ -145,10 +144,7 @@ export const spec = { 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); - } + triggerNurlWithCpm(bid, cpm) } }; diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index c7e8102ffc9..1ea2f2c1ce6 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -2,7 +2,7 @@ * Adapter to send bids to Undertone */ -import {deepAccess, parseUrl} from '../src/utils.js'; +import {deepAccess, parseUrl, extractDomainFromHost} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; @@ -26,24 +26,6 @@ function getBidFloor(bidRequest, mediaType) { return (floor && floor.currency === 'USD' && floor.floor) || 0; } -function extractDomainFromHost(pageHost) { - let domain = null; - try { - let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); - if (domains != null && domains.length > 0) { - domain = domains[0]; - for (let i = 1; i < domains.length; i++) { - if (domains[i].length > domain.length) { - domain = domains[i]; - } - } - } - } catch (e) { - domain = null; - } - return domain; -} - function getGdprQueryParams(gdprConsent) { if (!gdprConsent) { return null; diff --git a/src/utils.js b/src/utils.js index ec227c6d74b..64880b4a462 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1258,3 +1258,31 @@ export function setOnAny(collection, key) { } return undefined; } + +export function extractDomainFromHost(pageHost) { + let domain = null; + try { + let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); + if (domains != null && domains.length > 0) { + domain = domains[0]; + for (let i = 1; i < domains.length; i++) { + if (domains[i].length > domain.length) { + domain = domains[i]; + } + } + } + } catch (e) { + domain = null; + } + return domain; +} + +export function triggerNurlWithCpm(bid, cpm) { + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace( + /\${AUCTION_PRICE}/, + cpm + ); + triggerPixel(bid.nurl); + } +} From cbec10c836f83267149ef89331c5672e7a9fb12e Mon Sep 17 00:00:00 2001 From: Pubrise Date: Wed, 31 Jul 2024 20:29:53 +0300 Subject: [PATCH 22/51] new adapter (#12067) --- modules/pubriseBidAdapter.js | 19 + modules/pubriseBidAdapter.md | 79 +++ test/spec/modules/pubriseBidAdapter_spec.js | 514 ++++++++++++++++++++ 3 files changed, 612 insertions(+) create mode 100644 modules/pubriseBidAdapter.js create mode 100755 modules/pubriseBidAdapter.md create mode 100644 test/spec/modules/pubriseBidAdapter_spec.js diff --git a/modules/pubriseBidAdapter.js b/modules/pubriseBidAdapter.js new file mode 100644 index 00000000000..646546329db --- /dev/null +++ b/modules/pubriseBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'pubrise'; +const AD_URL = 'https://backend.pubrise.ai/pbjs'; +const SYNC_URL = 'https://sync.pubrise.ai'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/pubriseBidAdapter.md b/modules/pubriseBidAdapter.md new file mode 100755 index 00000000000..4c130954392 --- /dev/null +++ b/modules/pubriseBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Pubrise Bidder Adapter +Module Type: Pubrise Bidder Adapter +Maintainer: prebid@pubrise.ai +``` + +# Description + +Connects to Pubrise exchange for bids. +Pubrise 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: 'pubrise', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pubrise', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pubrise', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/pubriseBidAdapter_spec.js b/test/spec/modules/pubriseBidAdapter_spec.js new file mode 100644 index 00000000000..6374c4f1b7f --- /dev/null +++ b/test/spec/modules/pubriseBidAdapter_spec.js @@ -0,0 +1,514 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/pubriseBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'pubrise'; + +describe('PubriseBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner' + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo' + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative' + }, + userIdAsEids + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, + refererInfo: { + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK' + } + }, + 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://backend.pubrise.ai/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys( + 'deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('object'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + 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 valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids + } + ]; + + let serverRequest = spec.buildRequests(bids, bidderRequest); + + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.endpointId).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('network'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('object'); + expect(data.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; + }); + + 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('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let 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 = bidderRequest.ortb2 || {}; + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let 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'); + + bidderRequest.ortb2; + }) + }); + + 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://sync.pubrise.ai/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + 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://sync.pubrise.ai/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + 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://sync.pubrise.ai/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); From 2f43ed6e85d19f2efbe9bc31189df5ed7c75fe47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Millet?= Date: Wed, 31 Jul 2024 19:35:17 +0200 Subject: [PATCH 23/51] Dailymotion bid adapter: add player name (#12068) This new parameter allows the Prebid integration to send which player will be displaying the video and the ad. This value is then processed server-side to validate integration and distinguish the usage of video metadata. As we are now able to differentiate which player is used, we also deprecate the Dailymotion-only `xid` parameter in favor of the standard `video.id` one. --- modules/dailymotionBidAdapter.js | 2 +- modules/dailymotionBidAdapter.md | 18 ++++++++------ .../modules/dailymotionBidAdapter_spec.js | 24 +++++++++---------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/modules/dailymotionBidAdapter.js b/modules/dailymotionBidAdapter.js index 1e40d72f780..eccdbbbe982 100644 --- a/modules/dailymotionBidAdapter.js +++ b/modules/dailymotionBidAdapter.js @@ -64,7 +64,6 @@ function getVideoMetadata(bidRequest, bidderRequest) { title: videoParams.title || deepAccess(contentObj, 'title', ''), url: videoParams.url || deepAccess(contentObj, 'url', ''), topics: videoParams.topics || '', - xid: videoParams.xid || '', isCreatedForKids: typeof videoParams.isCreatedForKids === 'boolean' ? videoParams.isCreatedForKids : null, @@ -79,6 +78,7 @@ function getVideoMetadata(bidRequest, bidderRequest) { autoplay: typeof videoParams.autoplay === 'boolean' ? videoParams.autoplay : null, + playerName: videoParams.playerName || deepAccess(contentObj, 'playerName', ''), playerVolume: ( typeof videoParams.playerVolume === 'number' && videoParams.playerVolume >= 0 && diff --git a/modules/dailymotionBidAdapter.md b/modules/dailymotionBidAdapter.md index 7ff3fd47d74..21cc6c49205 100644 --- a/modules/dailymotionBidAdapter.md +++ b/modules/dailymotionBidAdapter.md @@ -88,7 +88,7 @@ Please note that failing to set these will result in the adapter not bidding at To allow better targeting, you should provide as much context about the video as possible. There are three ways of doing this depending on if you're using Dailymotion player or a third party one. -If you are using the Dailymotion player, you should only provide the video `xid` in your ad unit, example: +If you are using the Dailymotion player, you must provide the video `xid` in the `video.id` field of your ad unit, example: ```javascript const adUnits = [ @@ -98,7 +98,10 @@ const adUnits = [ params: { apiKey: 'dailymotion-testing', video: { - xid: 'x123456' // Dailymotion infrastructure unique video ID + id: 'x123456' // Dailymotion infrastructure unique video ID + autoplay: false, + playerName: 'dailymotion', + playerVolume: 8 }, } }], @@ -117,9 +120,9 @@ const adUnits = [ ``` This will automatically fetch the most up-to-date information about the video. -If you provide any other metadata in addition to the `xid`, they will be ignored. +Please note that if you provide any video metadata not listed above, they will be replaced by the ones fetched from the `video.id`. -If you are using a third party video player, you should not provide any `xid` and instead fill the following members: +If you are using a third party video player, you should fill the following members: ```javascript const adUnits = [ @@ -143,6 +146,7 @@ const adUnits = [ isCreatedForKids: false, videoViewsInSession: 1, autoplay: false, + playerName: 'video.js', playerVolume: 8 } } @@ -181,14 +185,14 @@ Each of the following video metadata fields can be added in bids.params.video. * `title` - Video title * `url` - URL of the content * `topics` - Main topics for the video, comma separated -* `xid` - Dailymotion video identifier (only applicable if using the Dailymotion player) * `isCreatedForKids` - [The content is created for children as primary audience](https://faq.dailymotion.com/hc/en-us/articles/360020920159-Content-created-for-kids) -The following contextual informations can also be added in bids.params.video. +The following contextual information can also be added in bids.params.video. -* `videoViewsInSession` - Number of videos viewed within the current user session * `autoplay` - Playback was launched without user interaction +* `playerName` - Name of the player used to display the video * `playerVolume` - Player volume between 0 (muted, 0%) and 10 (100%) +* `videoViewsInSession` - Number of videos viewed within the current user session If you already specify [First-Party data](https://docs.prebid.org/features/firstPartyData.html) through the `ortb2` object when calling [`pbjs.requestBids(requestObj)`](https://docs.prebid.org/dev-docs/publisher-api-reference/requestBids.html), we will collect the following values and fallback to bids.params.video values when applicable. See the mapping below. diff --git a/test/spec/modules/dailymotionBidAdapter_spec.js b/test/spec/modules/dailymotionBidAdapter_spec.js index 74e94d2444e..750bf340261 100644 --- a/test/spec/modules/dailymotionBidAdapter_spec.js +++ b/test/spec/modules/dailymotionBidAdapter_spec.js @@ -116,11 +116,11 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, isCreatedForKids: true, videoViewsInSession: 2, autoplay: true, + playerName: 'dailymotion', playerVolume: 8, }, }, @@ -204,7 +204,6 @@ describe('dailymotionBidAdapterTests', () => { title: bidRequestData[0].params.video.title, url: bidRequestData[0].params.video.url, topics: bidRequestData[0].params.video.topics, - xid: bidRequestData[0].params.video.xid, duration: bidRequestData[0].params.video.duration, livestream: !!bidRequestData[0].params.video.livestream, isCreatedForKids: bidRequestData[0].params.video.isCreatedForKids, @@ -212,6 +211,7 @@ describe('dailymotionBidAdapterTests', () => { siteOrAppCat: [], videoViewsInSession: bidRequestData[0].params.video.videoViewsInSession, autoplay: bidRequestData[0].params.video.autoplay, + playerName: bidRequestData[0].params.video.playerName, playerVolume: bidRequestData[0].params.video.playerVolume, }, }); @@ -254,11 +254,11 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, isCreatedForKids: true, videoViewsInSession: 2, autoplay: true, + playerName: 'dailymotion', playerVolume: 8, }, }, @@ -344,11 +344,11 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, isCreatedForKids: true, videoViewsInSession: 2, autoplay: true, + playerName: 'dailymotion', playerVolume: 8, }, }, @@ -431,11 +431,11 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, isCreatedForKids: true, videoViewsInSession: 2, autoplay: true, + playerName: 'dailymotion', playerVolume: 8, }, }, @@ -537,11 +537,11 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, isCreatedForKids: true, videoViewsInSession: 2, autoplay: true, + playerName: 'dailymotion', playerVolume: 8, }, }, @@ -645,11 +645,11 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, isCreatedForKids: true, videoViewsInSession: 2, autoplay: true, + playerName: 'dailymotion', playerVolume: 8, }, }, @@ -745,12 +745,12 @@ describe('dailymotionBidAdapterTests', () => { title: 'test video', url: 'https://test.com/test', topics: 'topic_1, topic_2', - xid: 'x123456', livestream: 1, // Test invalid values isCreatedForKids: 'false', videoViewsInSession: -1, autoplay: 'true', + playerName: 'dailymotion', playerVolume: 12, }, }, @@ -855,7 +855,6 @@ describe('dailymotionBidAdapterTests', () => { title: bidRequestData[0].params.video.title, url: bidRequestData[0].params.video.url, topics: bidRequestData[0].params.video.topics, - xid: bidRequestData[0].params.video.xid, // Overriden through bidder params duration: bidderRequestData.ortb2.app.content.len, livestream: !!bidRequestData[0].params.video.livestream, @@ -864,6 +863,7 @@ describe('dailymotionBidAdapterTests', () => { siteOrAppCat: [], videoViewsInSession: null, autoplay: null, + playerName: 'dailymotion', playerVolume: null, }, }); @@ -889,10 +889,10 @@ describe('dailymotionBidAdapterTests', () => { private: false, title: 'test video', topics: 'topic_1, topic_2', - xid: 'x123456', isCreatedForKids: false, videoViewsInSession: 10, autoplay: false, + playerName: 'dailymotion', playerVolume: 0, }, }, @@ -1035,7 +1035,6 @@ describe('dailymotionBidAdapterTests', () => { title: bidderRequestData.ortb2.site.content.title, url: bidderRequestData.ortb2.site.content.url, topics: bidRequestData[0].params.video.topics, - xid: bidRequestData[0].params.video.xid, duration: bidRequestData[0].params.video.duration, livestream: !!bidderRequestData.ortb2.site.content.livestream, isCreatedForKids: bidRequestData[0].params.video.isCreatedForKids, @@ -1043,6 +1042,7 @@ describe('dailymotionBidAdapterTests', () => { siteOrAppCat: bidderRequestData.ortb2.site.content.cat, videoViewsInSession: bidRequestData[0].params.video.videoViewsInSession, autoplay: bidRequestData[0].params.video.autoplay, + playerName: bidRequestData[0].params.video.playerName, playerVolume: bidRequestData[0].params.video.playerVolume, }, }); @@ -1127,13 +1127,13 @@ describe('dailymotionBidAdapterTests', () => { title: '', url: '', topics: '', - xid: '', livestream: false, isCreatedForKids: null, context: { siteOrAppCat: [], videoViewsInSession: null, autoplay: null, + playerName: '', playerVolume: null, }, }); From 62e6d89173b535f05f864f0d3252f9948cea79c4 Mon Sep 17 00:00:00 2001 From: Milica <110067445+GMilica@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:35:58 +0200 Subject: [PATCH 24/51] Update cwire adapter for new inventory management (#12066) --- modules/cwireBidAdapter.js | 22 ++++++++++++--------- modules/cwireBidAdapter.md | 39 ++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 191ff7758da..6fbe401bfde 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -151,14 +151,18 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!bid.params?.placementId || !isNumber(bid.params.placementId)) { - logError('placementId not provided or not a number'); - return false; - } + if (!bid.params?.domainId || !isNumber(bid.params.domainId)) { + logError('domainId not provided or not a number'); + if (!bid.params?.placementId || !isNumber(bid.params.placementId)) { + logError('placementId not provided or not a number'); + return false; + } - if (!bid.params?.pageId || !isNumber(bid.params.pageId)) { - logError('pageId not provided or not a number'); - return false; + if (!bid.params?.pageId || !isNumber(bid.params.pageId)) { + logError('pageId not provided or not a number'); + return false; + } + return true; } return true; }, @@ -176,8 +180,8 @@ export const spec = { // process bid requests let processed = validBidRequests .map(bid => slotDimensions(bid)) - // Flattens the pageId and placement Id for backwards compatibility. - .map((bid) => ({...bid, pageId: bid.params?.pageId, placementId: bid.params?.placementId})); + // Flattens the pageId, domainId and placement Id for backwards compatibility. + .map((bid) => ({...bid, pageId: bid.params?.pageId, domainId: bid.params?.domainId, placementId: bid.params?.placementId})); const extensions = getCwExtension(); const payload = { diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index 9804250b906..1d4f3c039c8 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -14,14 +14,15 @@ Prebid.js Adapter for C-Wire. Below, the list of C-WIRE params and where they can be set. -| Param name | URL parameter | AdUnit config | Type | Required | -|-------------|:-------------:|:-------------:|:--------:|:-------------:| -| pageId | | x | number | YES | -| placementId | | x | number | YES | -| cwgroups | x | | string | NO | -| cwcreative | x | | string | NO | -| cwdebug | x | | boolean | NO | -| cwfeatures | x | | string | NO | +| Param name | URL parameter | AdUnit config | Type | Required | +|-------------|:-------------:|:-------------:|:--------:|:--------:| +| pageId | | x | number | NO | +| domainId | | x | number | YES | +| placementId | | x | number | NO | +| cwgroups | x | | string | NO | +| cwcreative | x | | string | NO | +| cwdebug | x | | boolean | NO | +| cwfeatures | x | | string | NO | ### adUnit configuration @@ -38,12 +39,30 @@ var adUnits = [ } }, params: { - pageId: 1422, // required - number - placementId: 2211521, // required - number + domainId: 1422, // required - number + placementId: 2211521, // optional - number } }] } ]; +// old version for the compatibility +var adUnits = [ + { + code: 'target_div_id', // REQUIRED + bids: [{ + bidder: 'cwire', + mediaTypes: { + banner: { + sizes: [[400, 600]], + } + }, + params: { + pageId: 1422, // required - number + placementId: 2211521, // required - number + } + }] + } +]; ``` ### URL parameters From 39fa308345db8ee1b02a1c98cc95e116428a5b1c Mon Sep 17 00:00:00 2001 From: mjaworskiccx <50406214+mjaworskiccx@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:41:55 +0200 Subject: [PATCH 25/51] Ccx bid adapter: Protected Audence, add request param imp.ext.ae (#12055) * adomain support * adomain support * adomain support * adomain support * adomain support * video params * docs changes * Clickonometrics adapter update * Revert "Revert "Clickonometrics Bid Adapter : add gvlid (#9198)" (#9216)" This reverts commit 6d114e83725b403fadd889202b449de225db7275. * Test fix * tests * fledge * fledge --------- Co-authored-by: Michal Jaworski <76069238+mkjsoft@users.noreply.github.com> --- modules/ccxBidAdapter.js | 8 ++- test/spec/modules/ccxBidAdapter_spec.js | 81 +++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index b1fcb29e3d0..0a305a651cb 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -65,7 +65,7 @@ function _validateSizes (sizeObj, type) { return true } -function _buildBid (bid) { +function _buildBid (bid, bidderRequest) { let placement = {} placement.id = bid.bidId placement.secure = 1 @@ -105,6 +105,10 @@ function _buildBid (bid) { placement.ext = {'pid': bid.params.placementId} + if (bidderRequest.paapi?.enabled) { + placement.ext.ae = bid?.ortb2Imp?.ext?.ae + } + return placement } @@ -197,7 +201,7 @@ export const spec = { } _each(validBidRequests, function (bid) { - requestBody.imp.push(_buildBid(bid)) + requestBody.imp.push(_buildBid(bid, bidderRequest)) }) // Return the server request return { diff --git a/test/spec/modules/ccxBidAdapter_spec.js b/test/spec/modules/ccxBidAdapter_spec.js index cbae441e7e7..1e345691bcf 100644 --- a/test/spec/modules/ccxBidAdapter_spec.js +++ b/test/spec/modules/ccxBidAdapter_spec.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd'; import { spec } from 'modules/ccxBidAdapter.js'; import * as utils from 'src/utils.js'; @@ -39,6 +40,7 @@ describe('ccxAdapter', function () { transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' } ]; + describe('isBidRequestValid', function () { it('Valid bid requests', function () { expect(spec.isBidRequestValid(bids[0])).to.be.true; @@ -75,6 +77,7 @@ describe('ccxAdapter', function () { expect(spec.isBidRequestValid(bidsClone[0])).to.be.true; }); }); + describe('buildRequests', function () { it('No valid bids', function () { expect(spec.buildRequests([])).to.be.undefined; @@ -173,6 +176,7 @@ describe('ccxAdapter', function () { expect(data.imp).to.deep.have.same.members(imps); }); + it('Valid bid request - sizes old style', function () { let bidsClone = utils.deepClone(bids); delete (bidsClone[0].mediaTypes); @@ -218,6 +222,7 @@ describe('ccxAdapter', function () { expect(data.imp).to.deep.have.same.members(imps); }); + it('Valid bid request - sizes old style - no media type', function () { let bidsClone = utils.deepClone(bids); delete (bidsClone[0].mediaTypes); @@ -385,6 +390,7 @@ describe('ccxAdapter', function () { expect(spec.interpretResponse({})).to.be.empty; }); }); + describe('getUserSyncs', function () { it('Valid syncs - all', function () { let syncOptions = { @@ -434,6 +440,7 @@ describe('ccxAdapter', function () { expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.be.empty; }); }); + describe('mediaTypesVideoParams', function () { it('Valid video mediaTypes', function () { let bids = [ @@ -488,4 +495,78 @@ describe('ccxAdapter', function () { expect(data.imp).to.deep.have.same.members(imps); }); }); + + describe('FLEDGE', function () { + it('should properly build a request when FLEDGE is enabled', function () { + let bidderRequest = { + paapi: { + enabled: true + } + }; + let bids = [ + { + adUnitCode: 'banner', + auctionId: '0b9de793-8eda-481e-a548-aaaaaaaaaaa1', + bidId: '2e56e1af51ccc1', + bidder: 'ccx', + bidderRequestId: '17e7b9f58accc1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 609 + }, + sizes: [[300, 250]], + transactionId: 'befddd38-cfa0-48ab-8bdd-bbbbbbbbbbb1', + ortb2Imp: { + ext: { + ae: 1 + } + } + } + ]; + + let ortbRequest = spec.buildRequests(bids, syncAddFPDToBidderRequest(bidderRequest)); + let data = JSON.parse(ortbRequest.data); + expect(data.imp[0].ext.ae).to.equal(1); + }); + + it('should properly build a request when FLEDGE is disabled', function () { + let bidderRequest = { + paapi: { + enabled: false + } + }; + let bids = [ + { + adUnitCode: 'banner', + auctionId: '0b9de793-8eda-481e-a548-aaaaaaaaaaa2', + bidId: '2e56e1af51ccc2', + bidder: 'ccx', + bidderRequestId: '17e7b9f58accc2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 610 + }, + sizes: [[300, 250]], + transactionId: 'befddd38-cfa0-48ab-8bdd-bbbbbbbbbbb2', + ortb2Imp: { + ext: { + ae: 1 + } + } + } + ]; + + let ortbRequest = spec.buildRequests(bids, syncAddFPDToBidderRequest(bidderRequest)); + let data = JSON.parse(ortbRequest.data); + expect(data.imp[0].ext.ae).to.be.undefined; + }); + }); }); From 0d021fe4714e2180ef6fd26e97d9309d92b1854e Mon Sep 17 00:00:00 2001 From: Phaneendra Hegde Date: Wed, 31 Jul 2024 23:12:20 +0530 Subject: [PATCH 26/51] PubxAI Rtd module update: Make the endpoint call optional; read from local storage. (#12064) * Made RTD endpoint optional. Read the floors from local storage in case dynamic endpoint is not provided. * Updated specs to include localStorage tests * fix import error * use sessionStorage to fetchfloorData * updated tests * Fix the example code in the documentation * fix unit tests --------- Co-authored-by: tej656 --- modules/pubxaiRtdProvider.js | 56 ++++++++++++++------- modules/pubxaiRtdProvider.md | 4 +- test/spec/modules/pubxaiRtdProvider_spec.js | 42 ++++++++++++++-- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/modules/pubxaiRtdProvider.js b/modules/pubxaiRtdProvider.js index b958856df00..4528b29cf11 100644 --- a/modules/pubxaiRtdProvider.js +++ b/modules/pubxaiRtdProvider.js @@ -2,6 +2,8 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { submodule } from '../src/hook.js'; import { deepAccess } from '../src/utils.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; /** * This RTD module has a dependency on the priceFloors module. * We utilize the createFloorsDataForAuction function from the priceFloors module to incorporate price floors data into the current auction. @@ -16,6 +18,7 @@ export const FloorsApiStatus = Object.freeze({ SUCCESS: 'SUCCESS', ERROR: 'ERROR', }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); export const FLOORS_EVENT_HANDLE = 'floorsApi'; export const FLOORS_END_POINT = 'https://floor.pbxai.com/'; export const FLOOR_PROVIDER = 'PubxFloorProvider'; @@ -80,31 +83,48 @@ export const setFloorsApiStatus = (status) => { export const getUrl = (provider) => { const { pubxId, endpoint } = deepAccess(provider, 'params'); - return `${endpoint || FLOORS_END_POINT}?pubxId=${pubxId}&page=${ - window.location.href - }`; + if (!endpoint) { + return null; // Indicate that no endpoint is provided + } + return `${endpoint || FLOORS_END_POINT}?pubxId=${pubxId}&page=${window.location.href}`; }; export const fetchFloorRules = async (provider) => { return new Promise((resolve, reject) => { setFloorsApiStatus(FloorsApiStatus.IN_PROGRESS); - ajax(getUrl(provider), { - success: (responseText, response) => { - try { - if (response && response.response) { - const floorsResponse = JSON.parse(response.response); - resolve(floorsResponse); - } else { - resolve(null); + const url = getUrl(provider); + if (url) { + // Fetch from remote endpoint + ajax(url, { + success: (responseText, response) => { + try { + if (response && response.response) { + const floorsResponse = JSON.parse(response.response); + resolve(floorsResponse); + } else { + resolve(null); + } + } catch (error) { + reject(error); } - } catch (error) { - reject(error); + }, + error: (responseText, response) => { + reject(response); + }, + }); + } else { + // Fetch from local storage + try { + const localData = storage.getDataFromSessionStorage('pubx:dynamicFloors') || window.__pubxDynamicFloors__; + if (localData) { + resolve(JSON.parse(localData)); + } else { + resolve(null); } - }, - error: (responseText, response) => { - reject(response); - }, - }); + } catch (error) { + reject(error); + } + } }); }; diff --git a/modules/pubxaiRtdProvider.md b/modules/pubxaiRtdProvider.md index d7d89857c62..2b89d3baa04 100644 --- a/modules/pubxaiRtdProvider.md +++ b/modules/pubxaiRtdProvider.md @@ -32,7 +32,7 @@ pbjs.setConfig({ ..., realTimeData: { auctionDelay: AUCTION_DELAY, - dataProviders: { + dataProviders: [{ name: "pubxai", waitForIt: true, params: { @@ -42,7 +42,7 @@ pbjs.setConfig({ enforcement: ``, // (optional) data: `` // (optional) } - } + }] } // rest of the config ..., diff --git a/test/spec/modules/pubxaiRtdProvider_spec.js b/test/spec/modules/pubxaiRtdProvider_spec.js index b645b830246..6ffa4952992 100644 --- a/test/spec/modules/pubxaiRtdProvider_spec.js +++ b/test/spec/modules/pubxaiRtdProvider_spec.js @@ -1,6 +1,7 @@ import * as priceFloors from '../../../modules/priceFloors'; import { FLOORS_END_POINT, + storage, FLOORS_EVENT_HANDLE, FloorsApiStatus, beforeInit, @@ -45,6 +46,7 @@ const resetGlobals = () => { window.__pubxFloorsConfig__ = undefined; window.__pubxFloorsApiStatus__ = undefined; window.__pubxFloorRulesPromise__ = null; + localStorage.removeItem('pubx:dynamicFloors'); }; const fakeServer = ( @@ -119,7 +121,7 @@ describe('pubxaiRtdProvider', () => { stub.restore(); }); it('createFloorsDataForAuction called once before and once after __pubxFloorRulesPromise__. Also getBidRequestData executed only once', async () => { - pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {}); + pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => { }); assert(priceFloors.createFloorsDataForAuction.calledOnce); await window.__pubxFloorRulesPromise__; assert(priceFloors.createFloorsDataForAuction.calledTwice); @@ -129,7 +131,7 @@ describe('pubxaiRtdProvider', () => { reqBidsConfigObj.auctionId ) ); - pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {}); + pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => { }); await window.__pubxFloorRulesPromise__; assert(priceFloors.createFloorsDataForAuction.calledTwice); }); @@ -137,6 +139,16 @@ describe('pubxaiRtdProvider', () => { describe('fetchFloorRules', () => { const providerConfig = getConfig(); const floorsResponse = getFloorsResponse(); + let storageStub; + + beforeEach(() => { + storageStub = sinon.stub(storage, 'getDataFromSessionStorage'); + }); + + afterEach(() => { + storageStub.restore(); + }); + it('success with floors response', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer(floorsResponse); @@ -145,6 +157,7 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + it('success with no floors response', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer(undefined); @@ -153,6 +166,7 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + it('API call error', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer(undefined, undefined, 404); @@ -167,6 +181,7 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + it('Wrong API response', (done) => { const promise = fetchFloorRules(providerConfig); fakeServer('floorsResponse'); @@ -181,6 +196,25 @@ describe('pubxaiRtdProvider', () => { done(); }); }); + + it('success with local data response', (done) => { + const localFloorsResponse = getFloorsResponse(); + storageStub.withArgs('pubx:dynamicFloors').returns(JSON.stringify(localFloorsResponse)); + const promise = fetchFloorRules({ params: {} }); + promise.then((res) => { + expect(res).to.deep.equal(localFloorsResponse); + done(); + }); + }); + + it('no local data response', (done) => { + storageStub.withArgs('pubx:dynamicFloors').returns(null); + const promise = fetchFloorRules({ params: {} }); + promise.then((res) => { + expect(res).to.deep.equal(null); + done(); + }); + }); }); describe('setPriceFloors', () => { const providerConfig = getConfig(); @@ -383,9 +417,7 @@ describe('pubxaiRtdProvider', () => { expect(FLOORS_END_POINT).to.equal('https://floor.pbxai.com/'); }); it('standard case', () => { - expect(getUrl(provider)).to.equal( - `https://floor.pbxai.com/?pubxId=12345&page=${window.location.href}` - ); + expect(getUrl(provider)).to.equal(null); }); it('custom url provided', () => { provider.params.endpoint = 'https://custom.floor.com/'; From 2ada5d131bd27896dc738ac2f96a164f89e71d82 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 31 Jul 2024 18:52:31 -0700 Subject: [PATCH 27/51] fix 8podAnalytics tests (#12071) --- modules/eightPodAnalyticsAdapter.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/modules/eightPodAnalyticsAdapter.js b/modules/eightPodAnalyticsAdapter.js index 6dab3b0c107..e91f9412ef5 100644 --- a/modules/eightPodAnalyticsAdapter.js +++ b/modules/eightPodAnalyticsAdapter.js @@ -63,7 +63,9 @@ let eightPodAnalytics = Object.assign(adapter({url: trackerUrl, analyticsType}), trackEvent(data, adUnitCode); }); - setInterval(sendEvents, 10_000); + if (!this._interval) { + this._interval = setInterval(sendEvents, 10_000); + } }, resetQueue() { queue = []; @@ -182,6 +184,16 @@ eightPodAnalytics.enableAnalytics = function (config) { eightPodAnalytics.eventSubscribe(); }; +eightPodAnalytics.disableAnalytics = ((orig) => { + return function () { + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + return orig.apply(this, arguments); + } +})(eightPodAnalytics.disableAnalytics) + /** * Register Analytics Adapter */ From c0d565845dd8749b794807287cc28f5c5d902644 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 1 Aug 2024 03:43:12 -0700 Subject: [PATCH 28/51] Multiple modules: extract deviceMemory / hardwareConcurrency to library, add codeQL warnings (#12070) * Custom codeQL rules / hardwareConcurrency and deviceMemory * move deviceMemory / hardwareConcurrency to a library * reuse library code for deviceMemory & co --- .github/codeql/codeql-config.yml | 3 ++ .github/codeql/queries/deviceMemory.ql | 14 ++++++++ .github/codeql/queries/hardwareConcurrency.ql | 14 ++++++++ .github/codeql/queries/prebid.qll | 36 +++++++++++++++++++ .github/codeql/queries/qlpack.yml | 8 +++++ .../navigatorData/navigatorData.js | 2 +- modules/discoveryBidAdapter.js | 2 +- modules/mediagoBidAdapter.js | 8 ++--- modules/teadsBidAdapter.js | 7 ++-- test/spec/modules/discoveryBidAdapter_spec.js | 2 +- 10 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 .github/codeql/queries/deviceMemory.ql create mode 100644 .github/codeql/queries/hardwareConcurrency.ql create mode 100644 .github/codeql/queries/prebid.qll create mode 100644 .github/codeql/queries/qlpack.yml rename src/fpd/navigator.js => libraries/navigatorData/navigatorData.js (99%) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 2e8465003e4..8d3788e8956 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -2,3 +2,6 @@ paths: - src - modules - libraries +queries: + - name: Prebid queries + uses: ./.github/codeql/queries diff --git a/.github/codeql/queries/deviceMemory.ql b/.github/codeql/queries/deviceMemory.ql new file mode 100644 index 00000000000..6f650abf0e1 --- /dev/null +++ b/.github/codeql/queries/deviceMemory.ql @@ -0,0 +1,14 @@ +/** + * @id prebid/device-memory + * @name Access to navigator.deviceMemory + * @kind problem + * @problem.severity warning + * @description Finds uses of deviceMemory + */ + +import prebid + +from SourceNode nav +where + nav = windowPropertyRead("navigator") +select nav.getAPropertyRead("deviceMemory"), "deviceMemory is an indicator of fingerprinting" diff --git a/.github/codeql/queries/hardwareConcurrency.ql b/.github/codeql/queries/hardwareConcurrency.ql new file mode 100644 index 00000000000..350dbd1ae81 --- /dev/null +++ b/.github/codeql/queries/hardwareConcurrency.ql @@ -0,0 +1,14 @@ +/** + * @id prebid/hardware-concurrency + * @name Access to navigator.hardwareConcurrency + * @kind problem + * @problem.severity warning + * @description Finds uses of hardwareConcurrency + */ + +import prebid + +from SourceNode nav +where + nav = windowPropertyRead("navigator") +select nav.getAPropertyRead("hardwareConcurrency"), "hardwareConcurrency is an indicator of fingerprinting" diff --git a/.github/codeql/queries/prebid.qll b/.github/codeql/queries/prebid.qll new file mode 100644 index 00000000000..02fb5adc93c --- /dev/null +++ b/.github/codeql/queries/prebid.qll @@ -0,0 +1,36 @@ +import javascript +import DataFlow + +SourceNode otherWindow() { + result = globalVarRef("top") or + result = globalVarRef("self") or + result = globalVarRef("parent") or + result = globalVarRef("frames").getAPropertyRead() or + result = DOM::documentRef().getAPropertyRead("defaultView") +} + +SourceNode connectedWindow(SourceNode win) { + result = win.getAPropertyRead("self") or + result = win.getAPropertyRead("top") or + result = win.getAPropertyRead("parent") or + result = win.getAPropertyRead("frames").getAPropertyRead() or + result = win.getAPropertyRead("document").getAPropertyRead("defaultView") +} + +SourceNode relatedWindow(SourceNode win) { + result = connectedWindow(win) or + result = relatedWindow+(connectedWindow(win)) +} + +SourceNode anyWindow() { + result = otherWindow() or + result = relatedWindow(otherWindow()) +} + +/* + Matches uses of property `prop` done on any window object. +*/ +SourceNode windowPropertyRead(string prop) { + result = globalVarRef(prop) or + result = anyWindow().getAPropertyRead(prop) +} diff --git a/.github/codeql/queries/qlpack.yml b/.github/codeql/queries/qlpack.yml new file mode 100644 index 00000000000..72e90d3de9b --- /dev/null +++ b/.github/codeql/queries/qlpack.yml @@ -0,0 +1,8 @@ +--- +library: false +warnOnImplicitThis: false +name: queries +version: 0.0.1 +dependencies: + codeql/javascript-all: ^1.1.1 + codeql/javascript-queries: ^1.1.0 diff --git a/src/fpd/navigator.js b/libraries/navigatorData/navigatorData.js similarity index 99% rename from src/fpd/navigator.js rename to libraries/navigatorData/navigatorData.js index 80025f88640..f1a34fc51eb 100644 --- a/src/fpd/navigator.js +++ b/libraries/navigatorData/navigatorData.js @@ -26,4 +26,4 @@ export function getDM(win = window) { dm = undefined; } return dm; -}; +} diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 696faa86a0a..41fac90d62a 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -2,11 +2,11 @@ import * as utils from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; -import { getHLen, getHC, getDM } from '../src/fpd/navigator.js'; import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink, getReferrer } from '../libraries/fpdUtils/pageInfo.js'; import { getDevice, getScreenSize } from '../libraries/fpdUtils/deviceInfo.js'; import { getBidFloor } from '../libraries/currencyUtils/floor.js'; import { transformSizes, normalAdSize } from '../libraries/sizeUtils/tranformSize.js'; +import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 5c6c62b95fe..0f05449eee8 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -9,6 +9,7 @@ import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLin import { getDevice } from '../libraries/fpdUtils/deviceInfo.js'; import { getBidFloor } from '../libraries/currencyUtils/floor.js'; import { transformSizes, normalAdSize } from '../libraries/sizeUtils/tranformSize.js'; +import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; // import { config } from '../src/config.js'; // import { isPubcidEnabled } from './pubCommonId.js'; @@ -230,7 +231,6 @@ function getParam(validBidRequests, bidderRequest) { const timeout = bidderRequest.timeout || 2000; const firstPartyData = bidderRequest.ortb2; - const topWindow = window.top; const title = getPageTitle(); const desc = getPageDescription(); const keywords = getPageKeywords(); @@ -267,12 +267,12 @@ function getParam(validBidRequests, bidderRequest) { 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, + hLen: getHLen(), }, device: { nbw: getConnectionDownLink(), - hc: topWindow.navigator?.hardwareConcurrency || undefined, - dm: topWindow.navigator?.deviceMemory || undefined, + hc: getHC(), + dm: getDM() } }, user: { diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 8f775f2580d..73ea5dae575 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -2,6 +2,7 @@ import {getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParame import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {isAutoplayEnabled} from '../libraries/autoplayDetection/autoplay.js'; +import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -65,11 +66,11 @@ export const spec = { deviceHeight: screen.height, devicePixelRatio: topWindow.devicePixelRatio, screenOrientation: screen.orientation?.type, - historyLength: topWindow.history?.length, + historyLength: getHLen(), viewportHeight: topWindow.visualViewport?.height, viewportWidth: topWindow.visualViewport?.width, - hardwareConcurrency: topWindow.navigator?.hardwareConcurrency, - deviceMemory: topWindow.navigator?.deviceMemory, + hardwareConcurrency: getHC(), + deviceMemory: getDM(), hb_version: '$prebid.version$', ...getSharedViewerIdParameters(validBidRequests), ...getFirstPartyTeadsIdParameter(validBidRequests) diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index ffb733a2cb2..f3b35e5cda6 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -10,7 +10,7 @@ import { } from 'modules/discoveryBidAdapter.js'; import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink } from '../../../libraries/fpdUtils/pageInfo.js'; import * as utils from 'src/utils.js'; -import { getHLen, getHC, getDM } from '../../../src/fpd/navigator.js'; +import {getDM, getHC, getHLen} from '../../../libraries/navigatorData/navigatorData.js'; describe('discovery:BidAdapterTests', function () { let sandbox; From b1fb6dd02cc6712c9e69a47fc1ac6b91c39af825 Mon Sep 17 00:00:00 2001 From: SmartHubSolutions <87376145+SmartHubSolutions@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:42:08 +0300 Subject: [PATCH 29/51] Smarthub: add alias FelixAds (#12072) * update adapter SmartHub: add aliases * SmartHub: add alias FelixAds --------- Co-authored-by: Victor Co-authored-by: Patrick McCann --- modules/smarthubBidAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js index baf4c358736..367011d68d5 100644 --- a/modules/smarthubBidAdapter.js +++ b/modules/smarthubBidAdapter.js @@ -12,6 +12,7 @@ const ALIASES = [ {code: 'markapp', skipPbsAliasing: true}, {code: 'jdpmedia', skipPbsAliasing: true}, {code: 'tredio', skipPbsAliasing: true}, + {code: 'felixads', skipPbsAliasing: true}, {code: 'vimayx', skipPbsAliasing: true}, ]; const BASE_URLS = { @@ -19,6 +20,7 @@ const BASE_URLS = { markapp: 'https://markapp-prebid.smart-hub.io/pbjs', jdpmedia: 'https://jdpmedia-prebid.smart-hub.io/pbjs', tredio: 'https://tredio-prebid.smart-hub.io/pbjs', + felixads: 'https://felixads-prebid.smart-hub.io/pbjs', vimayx: 'https://vimayx-prebid.smart-hub.io/pbjs', }; From 1947d97de1e5cac6dafff270b4ff570469d6e051 Mon Sep 17 00:00:00 2001 From: Oleksandr Yermakov <47504677+sanychtasher@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:53:20 +0300 Subject: [PATCH 30/51] increment version (#12075) Co-authored-by: Oleksandr Yermakov --- modules/mgidBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 70a9290aa4d..a384b9d676c 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -88,7 +88,7 @@ _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAs _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); export const spec = { - VERSION: '1.7', + VERSION: '1.8', code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE], From cf98f85fda395b70bfe5816b2481b318417aac5c Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 1 Aug 2024 12:41:45 -0700 Subject: [PATCH 31/51] ortbConverter: do not override EIDS provided as first party data (#12076) * ortbConverter: do not override EIDS provided as first party data * update tests --- modules/userId/index.js | 2 +- test/spec/modules/openxBidAdapter_spec.js | 2 +- .../modules/trafficgateBidAdapter_spec.js | 2 +- test/spec/ortbConverter/userId_spec.js | 25 +++++++++++++++++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index b8d013df1c6..31083fd1e47 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -1196,7 +1196,7 @@ module('userId', attachIdSystem, { postInstallAllowed: true }); export function setOrtbUserExtEids(ortbRequest, bidderRequest, context) { const eids = deepAccess(context, 'bidRequests.0.userIdAsEids'); if (eids && Object.keys(eids).length > 0) { - deepSetValue(ortbRequest, 'user.ext.eids', eids); + deepSetValue(ortbRequest, 'user.ext.eids', eids.concat(ortbRequest.user?.ext?.eids || [])); } } registerOrtbProcessor({type: REQUEST, name: 'userExtEids', fn: setOrtbUserExtEids}); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index ad4ee1e74ce..e895574b9aa 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1025,7 +1025,7 @@ describe('OpenxRtbAdapter', function () { // enrich bid request with userId key/value const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); - expect(request[0].data.user.ext.eids).to.equal(userIdAsEids); + expect(request[0].data.user.ext.eids).to.eql(userIdAsEids); }); it(`when no user ids are available, it should not send any extended ids`, function () { diff --git a/test/spec/modules/trafficgateBidAdapter_spec.js b/test/spec/modules/trafficgateBidAdapter_spec.js index 9c564606186..7fbc566d375 100644 --- a/test/spec/modules/trafficgateBidAdapter_spec.js +++ b/test/spec/modules/trafficgateBidAdapter_spec.js @@ -1006,7 +1006,7 @@ describe('TrafficgateOpenxRtbAdapter', function () { // enrich bid request with userId key/value const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); - expect(request[0].data.user.ext.eids).to.equal(userIdAsEids); + expect(request[0].data.user.ext.eids).to.eql(userIdAsEids); }); it(`when no user ids are available, it should not send any extended ids`, function () { diff --git a/test/spec/ortbConverter/userId_spec.js b/test/spec/ortbConverter/userId_spec.js index 04a4d39ee48..2084cb0cfae 100644 --- a/test/spec/ortbConverter/userId_spec.js +++ b/test/spec/ortbConverter/userId_spec.js @@ -6,13 +6,34 @@ describe('pbjs - ortb user eids', () => { setOrtbUserExtEids(req, {}, { bidRequests: [ { - userIdAsEids: {e: 'id'} + userIdAsEids: [{e: 'id'}] } ] }); - expect(req.user.ext.eids).to.eql({e: 'id'}); + expect(req.user.ext.eids).to.eql([{e: 'id'}]); }); + it('should not override eids from fpd', () => { + const req = { + user: { + ext: { + eids: [{existing: 'id'}] + } + } + }; + setOrtbUserExtEids(req, {}, { + bidRequests: [ + { + userIdAsEids: [{nw: 'id'}] + } + ] + }); + expect(req.user.ext.eids).to.eql([ + {nw: 'id'}, + {existing: 'id'}, + ]) + }) + it('has no effect if requests have no eids', () => { const req = {}; setOrtbUserExtEids(req, {}, [{}]); From 0b63c8fd9d252cede1e6186a6e0b3cc4f5e08f9a Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Fri, 2 Aug 2024 15:06:14 +0000 Subject: [PATCH 32/51] Prebid 9.8.0 release --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19a83a7b6ec..e9a08ecc18c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "9.8.0-pre", + "version": "9.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "9.8.0-pre", + "version": "9.8.0", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.24.6", diff --git a/package.json b/package.json index db93bbb4317..b4725197258 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "9.8.0-pre", + "version": "9.8.0", "description": "Header Bidding Management Library", "main": "src/prebid.public.js", "exports": { From 33366a3f7d6eaa7ec8c0435ff13f933ebad91344 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Fri, 2 Aug 2024 15:06:14 +0000 Subject: [PATCH 33/51] Increment version to 9.9.0-pre --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9a08ecc18c..21ceda42b53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "9.8.0", + "version": "9.9.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "9.8.0", + "version": "9.9.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.24.6", diff --git a/package.json b/package.json index b4725197258..7502dc8794e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "9.8.0", + "version": "9.9.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.public.js", "exports": { From d5c4ecc1c0da536784f1ca7bee311e46b842bcab Mon Sep 17 00:00:00 2001 From: ehb-mtk <163182361+ehb-mtk@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:31:36 -0400 Subject: [PATCH 34/51] add mobian contextual variables directly to site.ext.data rather than creating a subobject (#12082) --- modules/mobianRtdProvider.js | 51 +++++++-- test/spec/modules/mobianRtdProvider_spec.js | 115 ++++++++++++++++---- 2 files changed, 134 insertions(+), 32 deletions(-) diff --git a/modules/mobianRtdProvider.js b/modules/mobianRtdProvider.js index 71b6ed2d51d..251a8fd53d8 100644 --- a/modules/mobianRtdProvider.js +++ b/modules/mobianRtdProvider.js @@ -22,7 +22,6 @@ export const mobianBrandSafetySubmodule = { function init() { return true; } - function getBidRequestData(bidReqConfig, callback, config) { const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global; const pageUrl = encodeURIComponent(getPageUrl()); @@ -54,25 +53,55 @@ function getBidRequestData(bidReqConfig, callback, config) { .filter(key => key.startsWith('emotion_') && response[key]) .map(key => key.replace('emotion_', '')); - const risk = { - 'risk': mobianRisk, - 'contentCategories': categories, - 'sentiment': sentiment, - 'emotions': emotions + const categoryFlags = { + adult: categories.includes('adult'), + arms: categories.includes('arms'), + crime: categories.includes('crime'), + death_injury: categories.includes('death_injury'), + piracy: categories.includes('piracy'), + hate_speech: categories.includes('hate_speech'), + obscenity: categories.includes('obscenity'), + drugs: categories.includes('drugs'), + spam: categories.includes('spam'), + terrorism: categories.includes('terrorism'), + debated_issue: categories.includes('debated_issue') + }; + + const emotionFlags = { + love: emotions.includes('love'), + joy: emotions.includes('joy'), + anger: emotions.includes('anger'), + surprise: emotions.includes('surprise'), + sadness: emotions.includes('sadness'), + fear: emotions.includes('fear') }; - resolve(risk); - deepSetValue(ortb2Site.ext, 'data.mobian', risk); - callback() + deepSetValue(ortb2Site.ext, 'data.mobianRisk', mobianRisk); + deepSetValue(ortb2Site.ext, 'data.mobianSentiment', sentiment); + + Object.entries(categoryFlags).forEach(([category, value]) => { + deepSetValue(ortb2Site.ext, `data.mobianCategory${category.charAt(0).toUpperCase() + category.slice(1)}`, value); + }); + + Object.entries(emotionFlags).forEach(([emotion, value]) => { + deepSetValue(ortb2Site.ext, `data.mobianEmotion${emotion.charAt(0).toUpperCase() + emotion.slice(1)}`, value); + }); + + resolve({ + risk: mobianRisk, + sentiment: sentiment, + categoryFlags: categoryFlags, + emotionFlags: emotionFlags + }); + callback(); }, error: function () { resolve({}); - callback() + callback(); } }); }); } - function getPageUrl() { return window.location.href; } diff --git a/test/spec/modules/mobianRtdProvider_spec.js b/test/spec/modules/mobianRtdProvider_spec.js index 43f6cbaf569..bf34cc6387f 100644 --- a/test/spec/modules/mobianRtdProvider_spec.js +++ b/test/spec/modules/mobianRtdProvider_spec.js @@ -12,7 +12,9 @@ describe('Mobian RTD Submodule', function () { ortb2Fragments: { global: { site: { - ext: {} + ext: { + data: {} + } } } } @@ -23,7 +25,7 @@ describe('Mobian RTD Submodule', function () { ajaxStub.restore(); }); - it('should return risk level when server responds with garm_risk', function () { + it('should set individual key-value pairs when server responds with garm_risk', function () { ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function(url, callbacks) { callbacks.success(JSON.stringify({ garm_risk: 'low', @@ -32,14 +34,37 @@ describe('Mobian RTD Submodule', function () { })); }); - return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((risk) => { - expect(risk).to.deep.equal({ + return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { + expect(result).to.deep.equal({ risk: 'low', - contentCategories: [], sentiment: 'positive', - emotions: ['joy'] + categoryFlags: { + adult: false, + arms: false, + crime: false, + death_injury: false, + piracy: false, + hate_speech: false, + obscenity: false, + drugs: false, + spam: false, + terrorism: false, + debated_issue: false + }, + emotionFlags: { + love: false, + joy: true, + anger: false, + surprise: false, + sadness: false, + fear: false + } + }); + expect(bidReqConfig.ortb2Fragments.global.site.ext.data).to.deep.include({ + mobianRisk: 'low', + mobianSentiment: 'positive', + mobianEmotionJoy: true }); - expect(bidReqConfig.ortb2Fragments.global.site.ext.data.mobian).to.deep.equal(risk); }); }); @@ -55,14 +80,40 @@ describe('Mobian RTD Submodule', function () { })); }); - return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((risk) => { - expect(risk).to.deep.equal({ + return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { + expect(result).to.deep.equal({ risk: 'medium', - contentCategories: ['arms', 'crime'], sentiment: 'negative', - emotions: ['anger', 'fear'] + categoryFlags: { + adult: false, + arms: true, + crime: true, + death_injury: false, + piracy: false, + hate_speech: false, + obscenity: false, + drugs: false, + spam: false, + terrorism: false, + debated_issue: false + }, + emotionFlags: { + love: false, + joy: false, + anger: true, + surprise: false, + sadness: false, + fear: true + } + }); + expect(bidReqConfig.ortb2Fragments.global.site.ext.data).to.deep.include({ + mobianRisk: 'medium', + mobianSentiment: 'negative', + mobianCategoryArms: true, + mobianCategoryCrime: true, + mobianEmotionAnger: true, + mobianEmotionFear: true }); - expect(bidReqConfig.ortb2Fragments.global.site.ext.data.mobian).to.deep.equal(risk); }); }); @@ -73,14 +124,36 @@ describe('Mobian RTD Submodule', function () { })); }); - return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((risk) => { - expect(risk).to.deep.equal({ + return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { + expect(result).to.deep.equal({ risk: 'unknown', - contentCategories: [], sentiment: 'neutral', - emotions: [] + categoryFlags: { + adult: false, + arms: false, + crime: false, + death_injury: false, + piracy: false, + hate_speech: false, + obscenity: false, + drugs: false, + spam: false, + terrorism: false, + debated_issue: false + }, + emotionFlags: { + love: false, + joy: false, + anger: false, + surprise: false, + sadness: false, + fear: false + } + }); + expect(bidReqConfig.ortb2Fragments.global.site.ext.data).to.deep.include({ + mobianRisk: 'unknown', + mobianSentiment: 'neutral' }); - expect(bidReqConfig.ortb2Fragments.global.site.ext.data.mobian).to.deep.equal(risk); }); }); @@ -89,8 +162,8 @@ describe('Mobian RTD Submodule', function () { callbacks.success('unexpected output not even of the right type'); }); const originalConfig = JSON.parse(JSON.stringify(bidReqConfig)); - return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((risk) => { - expect(risk).to.deep.equal({}); + return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { + expect(result).to.deep.equal({}); // Check that bidReqConfig hasn't been modified expect(bidReqConfig).to.deep.equal(originalConfig); }); @@ -101,8 +174,8 @@ describe('Mobian RTD Submodule', function () { callbacks.error(); }); - return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((risk) => { - expect(risk).to.deep.equal({}); + return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { + expect(result).to.deep.equal({}); }); }); }); From bfea83eb30c6d01381d36ad9cd31215ac2c89cbe Mon Sep 17 00:00:00 2001 From: ehb-mtk <163182361+ehb-mtk@users.noreply.github.com> Date: Sat, 3 Aug 2024 11:16:12 -0400 Subject: [PATCH 35/51] reduce cardinality of fields added to site.ext.data (#12085) --- modules/mobianRtdProvider.js | 47 +++--------- test/spec/modules/mobianRtdProvider_spec.js | 84 ++++----------------- 2 files changed, 25 insertions(+), 106 deletions(-) diff --git a/modules/mobianRtdProvider.js b/modules/mobianRtdProvider.js index 251a8fd53d8..d71948046b9 100644 --- a/modules/mobianRtdProvider.js +++ b/modules/mobianRtdProvider.js @@ -22,6 +22,7 @@ export const mobianBrandSafetySubmodule = { function init() { return true; } + function getBidRequestData(bidReqConfig, callback, config) { const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global; const pageUrl = encodeURIComponent(getPageUrl()); @@ -41,7 +42,7 @@ function getBidRequestData(bidReqConfig, callback, config) { let mobianRisk = response.garm_risk || 'unknown'; - const categories = Object.keys(response) + const contentCategories = Object.keys(response) .filter(key => key.startsWith('garm_content_category_') && response[key]) .map(key => key.replace('garm_content_category_', '')); @@ -53,46 +54,19 @@ function getBidRequestData(bidReqConfig, callback, config) { .filter(key => key.startsWith('emotion_') && response[key]) .map(key => key.replace('emotion_', '')); - const categoryFlags = { - adult: categories.includes('adult'), - arms: categories.includes('arms'), - crime: categories.includes('crime'), - death_injury: categories.includes('death_injury'), - piracy: categories.includes('piracy'), - hate_speech: categories.includes('hate_speech'), - obscenity: categories.includes('obscenity'), - drugs: categories.includes('drugs'), - spam: categories.includes('spam'), - terrorism: categories.includes('terrorism'), - debated_issue: categories.includes('debated_issue') - }; - - const emotionFlags = { - love: emotions.includes('love'), - joy: emotions.includes('joy'), - anger: emotions.includes('anger'), - surprise: emotions.includes('surprise'), - sadness: emotions.includes('sadness'), - fear: emotions.includes('fear') + const risk = { + risk: mobianRisk, + contentCategories: contentCategories, + sentiment: sentiment, + emotions: emotions }; deepSetValue(ortb2Site.ext, 'data.mobianRisk', mobianRisk); + deepSetValue(ortb2Site.ext, 'data.mobianContentCategories', contentCategories); deepSetValue(ortb2Site.ext, 'data.mobianSentiment', sentiment); + deepSetValue(ortb2Site.ext, 'data.mobianEmotions', emotions); - Object.entries(categoryFlags).forEach(([category, value]) => { - deepSetValue(ortb2Site.ext, `data.mobianCategory${category.charAt(0).toUpperCase() + category.slice(1)}`, value); - }); - - Object.entries(emotionFlags).forEach(([emotion, value]) => { - deepSetValue(ortb2Site.ext, `data.mobianEmotion${emotion.charAt(0).toUpperCase() + emotion.slice(1)}`, value); - }); - - resolve({ - risk: mobianRisk, - sentiment: sentiment, - categoryFlags: categoryFlags, - emotionFlags: emotionFlags - }); + resolve(risk); callback(); }, error: function () { @@ -102,6 +76,7 @@ function getBidRequestData(bidReqConfig, callback, config) { }); }); } + function getPageUrl() { return window.location.href; } diff --git a/test/spec/modules/mobianRtdProvider_spec.js b/test/spec/modules/mobianRtdProvider_spec.js index bf34cc6387f..717745c02f2 100644 --- a/test/spec/modules/mobianRtdProvider_spec.js +++ b/test/spec/modules/mobianRtdProvider_spec.js @@ -25,7 +25,7 @@ describe('Mobian RTD Submodule', function () { ajaxStub.restore(); }); - it('should set individual key-value pairs when server responds with garm_risk', function () { + it('should set key-value pairs when server responds with garm_risk', function () { ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function(url, callbacks) { callbacks.success(JSON.stringify({ garm_risk: 'low', @@ -37,33 +37,15 @@ describe('Mobian RTD Submodule', function () { return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { expect(result).to.deep.equal({ risk: 'low', + contentCategories: [], sentiment: 'positive', - categoryFlags: { - adult: false, - arms: false, - crime: false, - death_injury: false, - piracy: false, - hate_speech: false, - obscenity: false, - drugs: false, - spam: false, - terrorism: false, - debated_issue: false - }, - emotionFlags: { - love: false, - joy: true, - anger: false, - surprise: false, - sadness: false, - fear: false - } + emotions: ['joy'] }); expect(bidReqConfig.ortb2Fragments.global.site.ext.data).to.deep.include({ mobianRisk: 'low', + mobianContentCategories: [], mobianSentiment: 'positive', - mobianEmotionJoy: true + mobianEmotions: ['joy'] }); }); }); @@ -83,36 +65,15 @@ describe('Mobian RTD Submodule', function () { return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { expect(result).to.deep.equal({ risk: 'medium', + contentCategories: ['arms', 'crime'], sentiment: 'negative', - categoryFlags: { - adult: false, - arms: true, - crime: true, - death_injury: false, - piracy: false, - hate_speech: false, - obscenity: false, - drugs: false, - spam: false, - terrorism: false, - debated_issue: false - }, - emotionFlags: { - love: false, - joy: false, - anger: true, - surprise: false, - sadness: false, - fear: true - } + emotions: ['anger', 'fear'] }); expect(bidReqConfig.ortb2Fragments.global.site.ext.data).to.deep.include({ mobianRisk: 'medium', + mobianContentCategories: ['arms', 'crime'], mobianSentiment: 'negative', - mobianCategoryArms: true, - mobianCategoryCrime: true, - mobianEmotionAnger: true, - mobianEmotionFear: true + mobianEmotions: ['anger', 'fear'] }); }); }); @@ -127,32 +88,15 @@ describe('Mobian RTD Submodule', function () { return mobianBrandSafetySubmodule.getBidRequestData(bidReqConfig, {}, {}).then((result) => { expect(result).to.deep.equal({ risk: 'unknown', + contentCategories: [], sentiment: 'neutral', - categoryFlags: { - adult: false, - arms: false, - crime: false, - death_injury: false, - piracy: false, - hate_speech: false, - obscenity: false, - drugs: false, - spam: false, - terrorism: false, - debated_issue: false - }, - emotionFlags: { - love: false, - joy: false, - anger: false, - surprise: false, - sadness: false, - fear: false - } + emotions: [] }); expect(bidReqConfig.ortb2Fragments.global.site.ext.data).to.deep.include({ mobianRisk: 'unknown', - mobianSentiment: 'neutral' + mobianContentCategories: [], + mobianSentiment: 'neutral', + mobianEmotions: [] }); }); }); From 625185b6891f9661426080ae3ba09b4fb654d133 Mon Sep 17 00:00:00 2001 From: tudou <42998776+lhxx121@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:00:19 +0800 Subject: [PATCH 36/51] Discovery Bid Adapter: remove calls to navigator (#12088) * Discovery Bid Adapter: delete the fingerprint related code * Discovery Bid Adapter: delete the fingerprint related code --------- Co-authored-by: lvhuixin --- modules/discoveryBidAdapter.js | 4 +- modules/mediagoBidAdapter.js | 4 +- test/spec/modules/discoveryBidAdapter_spec.js | 48 +------------------ 3 files changed, 3 insertions(+), 53 deletions(-) diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 41fac90d62a..0dbb1a3a6cb 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -6,7 +6,7 @@ import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLin import { getDevice, getScreenSize } from '../libraries/fpdUtils/deviceInfo.js'; import { getBidFloor } from '../libraries/currencyUtils/floor.js'; import { transformSizes, normalAdSize } from '../libraries/sizeUtils/tranformSize.js'; -import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; +import { getHLen } from '../libraries/navigatorData/navigatorData.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -291,8 +291,6 @@ function getParam(validBidRequests, bidderRequest) { }, device: { nbw: getConnectionDownLink(), - hc: getHC(), - dm: getDM() } } } catch (error) {} diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 0f05449eee8..1811f62e6ba 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -9,7 +9,7 @@ import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLin import { getDevice } from '../libraries/fpdUtils/deviceInfo.js'; import { getBidFloor } from '../libraries/currencyUtils/floor.js'; import { transformSizes, normalAdSize } from '../libraries/sizeUtils/tranformSize.js'; -import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; +import { getHLen } from '../libraries/navigatorData/navigatorData.js'; // import { config } from '../src/config.js'; // import { isPubcidEnabled } from './pubCommonId.js'; @@ -271,8 +271,6 @@ function getParam(validBidRequests, bidderRequest) { }, device: { nbw: getConnectionDownLink(), - hc: getHC(), - dm: getDM() } }, user: { diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index f3b35e5cda6..e65243fd15a 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -10,7 +10,7 @@ import { } from 'modules/discoveryBidAdapter.js'; import { getPageTitle, getPageDescription, getPageKeywords, getConnectionDownLink } from '../../../libraries/fpdUtils/pageInfo.js'; import * as utils from 'src/utils.js'; -import {getDM, getHC, getHLen} from '../../../libraries/navigatorData/navigatorData.js'; +import {getHLen} from '../../../libraries/navigatorData/navigatorData.js'; describe('discovery:BidAdapterTests', function () { let sandbox; @@ -660,51 +660,5 @@ describe('discovery Bid Adapter Tests', function () { expect(result).be.undefined; }); }); - - describe('getHC', () => { - it('should return the correct value of hardwareConcurrency when accessible', () => { - const mockWindow = { - top: { - navigator: { - hardwareConcurrency: 4 - } - } - }; - const result = getHC(mockWindow); - expect(result).to.equal(4); - }); - it('should return undefined when accessing win.top.navigator.hardwareConcurrency throws an error', () => { - const mockWindow = { - get top() { - throw new Error('Access denied'); - } - }; - const result = getHC(mockWindow); - expect(result).be.undefined; - }); - }); - - describe('getDM', () => { - it('should return the correct value of deviceMemory when accessible', () => { - const mockWindow = { - top: { - navigator: { - deviceMemory: 4 - } - } - }; - const result = getDM(mockWindow); - expect(result).to.equal(4); - }); - it('should return undefined when accessing win.top.navigator.deviceMemory throws an error', () => { - const mockWindow = { - get top() { - throw new Error('Access denied'); - } - }; - const result = getDM(mockWindow); - expect(result).be.undefined; - }); - }); }); }); From 49cf6102bbff0d4ceac507d947e5dc33a46fbfbf Mon Sep 17 00:00:00 2001 From: Doceree-techStack <143162581+Doceree-techStack@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:40:16 +0530 Subject: [PATCH 37/51] Doceree AdManager Bid Adapter : changes in fields and test coverage (#12090) * Updated docereeAdManager bid adapter * Updated docereeAdManager bid adapter * Updated docereeAdManager bid adapter * Updated docereeAdManager bid adapter * Updated docereeAdManager bid adapter * Updated docereeAdManager bid adapter * Update docereeAdManagerBidAdapter.js * added test cases for payload formation in DocereeAdManager * Added support for publisherUrl * added some parameters --------- Co-authored-by: lokesh-doceree Co-authored-by: Patrick McCann --- modules/docereeAdManagerBidAdapter.js | 22 +++-- .../docereeAdManagerBidAdapter_spec.js | 96 +++++++++++++++++-- 2 files changed, 101 insertions(+), 17 deletions(-) diff --git a/modules/docereeAdManagerBidAdapter.js b/modules/docereeAdManagerBidAdapter.js index f97bfbba6eb..d115e785969 100644 --- a/modules/docereeAdManagerBidAdapter.js +++ b/modules/docereeAdManagerBidAdapter.js @@ -70,14 +70,19 @@ export const spec = { }, }; -function getPayload(bid, userData) { +export function getPayload(bid, userData) { if (!userData || !bid) { return false; } const { bidId, params } = bid; - const { placementId } = params; + const { placementId, publisherUrl } = params; const { userid, + email, + firstname, + lastname, + hcpid, + dob, specialization, gender, city, @@ -95,11 +100,11 @@ function getPayload(bid, userData) { const data = { userid: platformUid || userid || '', - email: '', - firstname: '', - lastname: '', + email: email || '', + firstname: firstname || '', + lastname: lastname || '', specialization: specialization || '', - hcpid: '', + hcpid: hcpid || '', gender: gender || '', city: city || '', state: state || '', @@ -113,9 +118,10 @@ function getPayload(bid, userData) { hashedmobile: hashedmobile || '', country: country || '', organization: organization || '', - dob: '', + dob: dob || '', userconsent: 1, - mobile: mobile || '' + mobile: mobile || '', + pageurl: publisherUrl || '' }; return { data, diff --git a/test/spec/modules/docereeAdManagerBidAdapter_spec.js b/test/spec/modules/docereeAdManagerBidAdapter_spec.js index e367aab9678..b231d5572bf 100644 --- a/test/spec/modules/docereeAdManagerBidAdapter_spec.js +++ b/test/spec/modules/docereeAdManagerBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/docereeAdManagerBidAdapter.js'; +import { spec, getPayload } from '../../../modules/docereeAdManagerBidAdapter.js'; import { config } from '../../../src/config.js'; describe('docereeadmanager', function () { @@ -7,25 +7,25 @@ describe('docereeadmanager', function () { docereeadmanager: { user: { data: { + userId: '', email: '', firstname: '', lastname: '', - mobile: '', specialization: '', - organization: '', hcpid: '', - dob: '', gender: '', city: '', state: '', - country: '', + zipcode: '', + hashedNPI: '', hashedhcpid: '', hashedemail: '', hashedmobile: '', - userid: '', - zipcode: '', - userconsent: '', - platformUid: '' + country: '', + organization: '', + dob: '', + platformUid: '', + mobile: '', }, }, }, @@ -35,6 +35,7 @@ describe('docereeadmanager', function () { bidder: 'docereeadmanager', params: { placementId: 'DOC-19-1', + publisherUrl: 'xxxxxx.com/xxxx', gdpr: '1', gdprconsent: 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', @@ -123,4 +124,81 @@ describe('docereeadmanager', function () { .empty; }); }); + + describe('payload', function() { + it('should return payload with the correct data', function() { + const data = { + userId: 'xxxxx', + email: 'xxxx@mail.com', + firstname: 'Xxxxx', + lastname: 'Xxxxxx', + specialization: 'Xxxxxxxxx', + hcpid: 'xxxxxxx', + gender: 'Xxxx', + city: 'Xxxxx', + state: 'Xxxxxx', + zipcode: 'XXXXXX', + hashedNPI: 'xxxxxx', + hashedhcpid: 'xxxxxxx', + hashedemail: 'xxxxxxx', + hashedmobile: 'xxxxxxx', + country: 'Xxxxxx', + organization: 'Xxxxxx', + dob: 'xx-xx-xxxx', + platformUid: 'Xx.xxx.xxxxxx', + mobile: 'XXXXXXXXXX', + } + bid = {...bid, params: {...bid.params, placementId: 'DOC-19-1'}} + const payload = getPayload(bid, data); + const payloadData = payload.data; + expect(payloadData).to.have.all.keys( + 'userid', + 'email', + 'firstname', + 'lastname', + 'specialization', + 'hcpid', + 'gender', + 'city', + 'state', + 'zipcode', + 'hashedNPI', + 'pb', + 'adunit', + 'requestId', + 'hashedhcpid', + 'hashedemail', + 'hashedmobile', + 'country', + 'organization', + 'dob', + 'userconsent', + 'mobile', + 'pageurl' + ); + expect(payloadData.userid).to.equal('Xx.xxx.xxxxxx'); + expect(payloadData.email).to.equal('xxxx@mail.com'); + expect(payloadData.firstname).to.equal('Xxxxx'); + expect(payloadData.lastname).to.equal('Xxxxxx'); + expect(payloadData.specialization).to.equal('Xxxxxxxxx'); + expect(payloadData.hcpid).to.equal('xxxxxxx'); + expect(payloadData.gender).to.equal('Xxxx'); + expect(payloadData.city).to.equal('Xxxxx'); + expect(payloadData.state).to.equal('Xxxxxx'); + expect(payloadData.zipcode).to.equal('XXXXXX'); + expect(payloadData.hashedNPI).to.equal('xxxxxx'); + expect(payloadData.pb).to.equal(1); + expect(payloadData.userconsent).to.equal(1); + expect(payloadData.dob).to.equal('xx-xx-xxxx'); + expect(payloadData.organization).to.equal('Xxxxxx'); + expect(payloadData.country).to.equal('Xxxxxx'); + expect(payloadData.hashedmobile).to.equal('xxxxxxx'); + expect(payloadData.hashedemail).to.equal('xxxxxxx'); + expect(payloadData.hashedhcpid).to.equal('xxxxxxx'); + expect(payloadData.requestId).to.equal('testing'); + expect(payloadData.mobile).to.equal('XXXXXXXXXX'); + expect(payloadData.adunit).to.equal('DOC-19-1'); + expect(payloadData.pageurl).to.equal('xxxxxx.com/xxxx'); + }) + }) }); From 8c4936a5e58a2713623e8b90f80f7552fc752db2 Mon Sep 17 00:00:00 2001 From: afifmahdi95 <55173899+afifmahdi95@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:12:59 +0700 Subject: [PATCH 38/51] Feature change on Prebid Request (#12089) Co-authored-by: Ahmad Afif Mahdi --- modules/pstudioBidAdapter.js | 13 +++++-------- modules/pstudioBidAdapter.md | 10 ++++++---- test/spec/modules/pstudioBidAdapter_spec.js | 10 +++++----- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/modules/pstudioBidAdapter.js b/modules/pstudioBidAdapter.js index 1265d5c546f..cc9310e174b 100644 --- a/modules/pstudioBidAdapter.js +++ b/modules/pstudioBidAdapter.js @@ -6,8 +6,6 @@ import { isNumber, generateUUID, isEmpty, - isFn, - isPlainObject, } from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -59,7 +57,7 @@ export const spec = { isBidRequestValid: function (bid) { const params = bid.params || {}; - return !!params.pubid && !!params.floorPrice && isVideoRequestValid(bid); + return !!params.pubid && !!params.adtagid && isVideoRequestValid(bid); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -145,7 +143,7 @@ function buildRequestData(bid, bidderRequest) { function buildBaseObject(bid, bidderRequest) { const firstPartyData = prepareFirstPartyData(bidderRequest.ortb2); - const { pubid, bcat, badv, bapp } = bid.params; + const { pubid, adtagid, bcat, badv, bapp } = bid.params; const { userId } = bid; const uid2Token = userId?.uid2?.id; @@ -168,8 +166,7 @@ function buildBaseObject(bid, bidderRequest) { return { id: bid.bidId, pubid, - floor_price: getBidFloor(bid), - adtagid: bid.adUnitCode, + adtagid: adtagid, ...(bcat && { bcat }), ...(badv && { badv }), ...(bapp && { bapp }), @@ -417,7 +414,7 @@ function validateSizes(sizes) { ); } -function getBidFloor(bid) { +/* function getBidFloor(bid) { if (!isFn(bid.getFloor)) { return bid.params.floorPrice ? bid.params.floorPrice : null; } @@ -431,6 +428,6 @@ function getBidFloor(bid) { return floor.floor; } return null; -} +} */ registerBidder(spec); diff --git a/modules/pstudioBidAdapter.md b/modules/pstudioBidAdapter.md index 0c8c6927f43..a4b3e098cfc 100644 --- a/modules/pstudioBidAdapter.md +++ b/modules/pstudioBidAdapter.md @@ -31,7 +31,8 @@ var adUnits = [ params: { // id of test publisher pubid: '22430f9d-9610-432c-aabe-6134256f11af', - floorPrice: 1.25, + // id of test adtag id + adtagid: '6f3173b9-5623-4a4f-8c62-2b1d24ceb4e6', }, }, ], @@ -53,7 +54,8 @@ var adUnits = [ params: { // id of test publisher pubid: '22430f9d-9610-432c-aabe-6134256f11af', - floorPrice: 1.25, + // id of test adtag id + adtagid: '097c601f-ad09-495b-b70b-d9cf6f1edbc1', }, }, ], @@ -79,7 +81,7 @@ var adUnits = [ bidder: 'pstudio', params: { pubid: '22430f9d-9610-432c-aabe-6134256f11af', // required - floorPrice: 1.15, // required + adtagid: 'b9be4c35-3c12-4fa9-96ba-34b90276208c', // required bcat: ['IAB1-1', 'IAB1-3'], // optional badv: ['nike.com'], // optional bapp: ['com.foo.mygame'], // optional @@ -121,7 +123,7 @@ var videoAdUnits = [ bidder: 'pstudio', params: { pubid: '22430f9d-9610-432c-aabe-6134256f11af', - floorPrice: 1.25, + adtagid: '46e348cf-b79d-43e5-81bc-5954cdf15d7e', badv: ['adidas.com'], }, }, diff --git a/test/spec/modules/pstudioBidAdapter_spec.js b/test/spec/modules/pstudioBidAdapter_spec.js index 52ecb820ed3..2ec38e8dcda 100644 --- a/test/spec/modules/pstudioBidAdapter_spec.js +++ b/test/spec/modules/pstudioBidAdapter_spec.js @@ -18,7 +18,7 @@ describe('PStudioAdapter', function () { bidder: 'pstudio', params: { pubid: '258c2a8d-d2ad-4c31-a2a5-e63001186456', - floorPrice: 1.15, + adtagid: 'aae1aabb-6699-4b5a-9c3f-9ed034b1932c', }, adUnitCode: 'test-div-1', mediaTypes: { @@ -38,7 +38,7 @@ describe('PStudioAdapter', function () { bidder: 'pstudio', params: { pubid: '258c2a8d-d2ad-4c31-a2a5-e63001186456', - floorPrice: 1.15, + adtagid: '34833639-f17c-40bc-9c4b-222b1b7459c7', }, adUnitCode: 'test-div-1', mediaTypes: { @@ -197,7 +197,7 @@ describe('PStudioAdapter', function () { it('should return false when publisher id not found', function () { const localBid = deepClone(bannerBid); delete localBid.params.pubid; - delete localBid.params.floorPrice; + delete localBid.params.adtagid; expect(spec.isBidRequestValid(localBid)).to.equal(false); }); @@ -232,7 +232,7 @@ describe('PStudioAdapter', function () { it('should properly map ids in request payload', function () { expect(bannerPayload.id).to.equal(bannerBid.bidId); - expect(bannerPayload.adtagid).to.equal(bannerBid.adUnitCode); + expect(bannerPayload.adtagid).to.equal(bannerBid.params.adtagid); }); it('should properly map banner mediaType in request payload', function () { @@ -266,7 +266,7 @@ describe('PStudioAdapter', function () { it('should properly set required bidder params in request payload', function () { expect(bannerPayload.pubid).to.equal(bannerBid.params.pubid); - expect(bannerPayload.floor_price).to.equal(bannerBid.params.floorPrice); + expect(bannerPayload.adtagid).to.equal(bannerBid.params.adtagid); }); it('should omit optional bidder params or first-party data from bid request if they are not provided', function () { From 781787e8bc576abe9ef4c8c39b5816e628181820 Mon Sep 17 00:00:00 2001 From: CMDezz <53512796+CMDezz@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:15:35 +0700 Subject: [PATCH 39/51] EClickAds Bid Adapter : initial release (#12087) * Initial new bid adapter: ElickAds * EClickAds: update bid adapter * EClickAds: Update getDevice func * EClickAds: update bidder --------- Co-authored-by: vietlv14 Co-authored-by: LeViet --- modules/eclickadsBidAdapter.js | 80 +++++++ modules/eclickadsBidAdapter.md | 55 +++++ test/spec/modules/eclickadsBidAdapter_spec.js | 214 ++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 modules/eclickadsBidAdapter.js create mode 100644 modules/eclickadsBidAdapter.md create mode 100644 test/spec/modules/eclickadsBidAdapter_spec.js diff --git a/modules/eclickadsBidAdapter.js b/modules/eclickadsBidAdapter.js new file mode 100644 index 00000000000..27e3926afe3 --- /dev/null +++ b/modules/eclickadsBidAdapter.js @@ -0,0 +1,80 @@ +import { NATIVE } from '../src/mediaTypes.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getDevice } from '../libraries/fpdUtils/deviceInfo.js'; + +// ***** ECLICKADS ADAPTER ***** +export const BIDDER_CODE = 'eclickads'; +const DEFAULT_CURRENCY = ['USD']; +const DEFAULT_TTL = 1000; +export const ENDPOINT = 'https://g.eclick.vn/rtb_hb_request?fosp_uid='; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [NATIVE], + isBidRequestValid: (bid) => { + return !!bid && !!bid.params && !!bid.bidder && !!bid.params.zid; + }, + buildRequests: (validBidRequests = [], bidderRequest) => { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + const ortb2ConfigFPD = bidderRequest.ortb2.site.ext?.data || {}; + const ortb2Device = bidderRequest.ortb2.device; + const ortb2Site = bidderRequest.ortb2.site; + + const isMobile = getDevice(); + const imp = []; + const fENDPOINT = ENDPOINT + (ortb2ConfigFPD.fosp_uid || ''); + const request = { + deviceWidth: ortb2Device.w, + deviceHeight: ortb2Device.h, + ua: ortb2Device.ua, + language: ortb2Device.language, + device: isMobile ? 'mobile' : 'desktop', + host: ortb2Site.domain, + page: ortb2Site.page, + imp, + myvne_id: ortb2ConfigFPD.myvne_id || '', + orig_aid: ortb2ConfigFPD.orig_aid, + fosp_aid: ortb2ConfigFPD.fosp_aid, + fosp_uid: ortb2ConfigFPD.fosp_uid, + id: ortb2ConfigFPD.id, + }; + + validBidRequests.map((bid) => { + imp.push({ + requestId: bid.bidId, + adUnitCode: bid.adUnitCode, + zid: bid.params.zid, + }); + }); + + return { + method: 'POST', + url: fENDPOINT, + data: request, + }; + }, + interpretResponse: (serverResponse) => { + const seatbid = serverResponse.body?.seatbid || []; + return seatbid.reduce((bids, bid) => { + return [ + ...bids, + { + id: bid.id, + impid: bid.impid, + adUnitCode: bid.adUnitCode, + cpm: bid.cpm, + ttl: bid.ttl || DEFAULT_TTL, + requestId: bid.requestId, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + currency: bid.currency || DEFAULT_CURRENCY, + adserverTargeting: { + hb_ad_eclickads: bid.ad, + }, + }, + ]; + }, []); + }, +}; +registerBidder(spec); diff --git a/modules/eclickadsBidAdapter.md b/modules/eclickadsBidAdapter.md new file mode 100644 index 00000000000..39ba19d5249 --- /dev/null +++ b/modules/eclickadsBidAdapter.md @@ -0,0 +1,55 @@ +# Overview + +Module Name: EClickAds Bid Adapter +Type: Bidder Adapter +Maintainer: vietlv14@fpt.com + +# Description + +This module connects to EClickAds exchange for bidding NATIVE ADS via prebid.js + +# Test Parameters + +``` +var adUnits = [{ + code: 'test-div', + mediaTypes: { + native: { + title: { + required: true, + len: 50 + }, + body: { + required: true, + len: 350 + }, + url: { + required: true + }, + image: { + required: true, + sizes : [300, 175] + }, + sponsoredBy: { + required: true + } + } + }, + bids: [{ + bidder: 'eclickads', + params: { + zid:"7096" + } + }] +}]; +``` + +# Notes: + +- EClickAdsBidAdapter need serveral params inside bidder config as following + - user.myvne_id + - site.orig_aid + - site.fosp_aid + - site.id + - site.orig_aid +- EClickAdsBidAdapter will set bid.adserverTargeting.hb_ad_eclickads targeting key while submitting bid to AdServer diff --git a/test/spec/modules/eclickadsBidAdapter_spec.js b/test/spec/modules/eclickadsBidAdapter_spec.js new file mode 100644 index 00000000000..aadb8f41a64 --- /dev/null +++ b/test/spec/modules/eclickadsBidAdapter_spec.js @@ -0,0 +1,214 @@ +import { expect } from 'chai'; +import { + spec, + ENDPOINT, + BIDDER_CODE, +} from '../../../modules/eclickadsBidAdapter.js'; +import { NATIVE, BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import { deepClone } from '../../../src/utils.js'; +import { config } from '../../../src/config.js'; + +describe('eclickadsBidAdapter', () => { + const bidItem = { + bidder: BIDDER_CODE, + params: { + zid: '7096', + }, + }; + const eclickadsBidderConfigData = { + orig_aid: 'xqf7zdmg7the65ac.1718271138.des', + fosp_aid: '1013000403', + fosp_uid: '7aab24a4663258a2c1d76a08b20f7e6e', + id: '84b2a41c4299bb9b8924423e', + myvne_id: '1013000403', + }; + const bidRequest = { + code: 'test-div', + size: [[320, 85]], + mediaTypes: { + [NATIVE]: { + title: { + required: true, + }, + body: { + required: true, + }, + image: { + required: true, + }, + sponsoredBy: { + required: true, + }, + icon: { + required: false, + }, + }, + }, + ortb2: { + device: { + w: 980, + h: 1720, + dnt: 0, + ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + language: 'en', + devicetype: 1, + make: 'Apple', + model: 'iPhone 12 Pro Max', + os: 'iOS', + osv: '17.4', + }, + site: { + name: 'example', + domain: 'page.example.com', + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + ext: { + data: eclickadsBidderConfigData, + }, + }, + }, + }; + + describe('isBidRequestValid', () => { + it('should return false when atleast one of required params is missing', () => { + const bid = deepClone(bidItem); + delete bid.params.zid; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('should return true if there is correct required params and mediatype', () => { + bidItem.params.mediaTypes == NATIVE; + expect(spec.isBidRequestValid(bidItem)).to.be.true; + }); + it('should return true if there is no size', () => { + const bid = deepClone(bidItem); + delete bid.params.size; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); + + describe('buildRequests', () => { + const bidList = [bidItem]; + const request = config.runWithBidder(BIDDER_CODE, () => + spec.buildRequests(bidList, bidRequest) + ); + const _data = request.data; + + it('should be create a request to server with POST method, data, valid url', () => { + expect(request).to.be.exist; + expect(_data).to.be.exist; + expect(request.method).to.be.exist; + expect(request.method).equal('POST'); + expect(request.url).to.be.exist; + expect(request.url).equal(ENDPOINT + eclickadsBidderConfigData.fosp_uid); + }); + it('should return valid data format if bid array is valid', () => { + expect(_data).to.be.an('object'); + expect(_data).to.has.all.keys( + 'deviceWidth', + 'deviceHeight', + 'language', + 'host', + 'ua', + 'page', + 'imp', + 'device', + 'myvne_id', + 'orig_aid', + 'fosp_aid', + 'fosp_uid', + 'id' + ); + expect(_data.deviceWidth).to.be.an('number'); + expect(_data.deviceHeight).to.be.an('number'); + expect(_data.device).to.be.an('string'); + expect(_data.language).to.be.an('string'); + expect(_data.host).to.be.an('string').that.is.equal('page.example.com'); + expect(_data.page) + .to.be.an('string') + .that.is.equal('https://page.example.com/here.html'); + expect(_data.imp).to.be.an('array'); + expect(_data.myvne_id).to.be.an('string'); + expect(_data.orig_aid).to.be.an('string'); + expect(_data.fosp_aid).to.be.an('string'); + expect(_data.myvne_id).to.be.an('string'); + expect(_data.fosp_uid).to.be.an('string'); + expect(_data.id).to.be.an('string'); + }); + + it('should return empty array if there is no bidItem passed', () => { + const request = config.runWithBidder(BIDDER_CODE, () => + spec.buildRequests([], bidRequest) + ); + const _data = request.data; + expect(_data.imp).to.be.an('array').that.is.empty; + }); + + it('should return the number of imp equal to the number of bidItem', () => { + expect(_data.imp).to.have.lengthOf(bidList.length); + }); + + it('have to contain required params and correct format for sending to EClickAds', () => { + const item = _data.imp[0]; + expect(item.zid).to.be.an('string'); + }); + }); + + describe('interpretResponse', () => { + const expectedResponse = { + id: '84b2a41c4299bb9b8924423e', + seat: '35809', + seatbid: [ + { + id: 'DBCCDFD5-AACC-424E-8225-4160D35CBE5D', + impid: '35ea1073c745d6c', + adUnitCode: '8871826dc92e', + requestId: '1122839202z3v', + creativeId: '112233ss921v', + netRevenue: true, + currency: ['VND'], + cpm: 0.1844, + ad: 'eclickads_ad_p', + }, + ], + }; + + const response = spec.interpretResponse({ body: expectedResponse }); + + it('should return an array of offers', () => { + expect(response).to.be.an('array'); + }); + + it('should return empty array if there is no offer from server response', () => { + const emptyOfferResponse = deepClone(expectedResponse); + emptyOfferResponse.seatbid = []; + const response = spec.interpretResponse({ body: emptyOfferResponse }); + expect(response).to.be.an('array').that.is.empty; + }); + + it('should return empty array if seatbid from server response is null or missing', () => { + const nullOfferResponse = deepClone(expectedResponse); + nullOfferResponse.seatbid = null; + const response = spec.interpretResponse({ body: nullOfferResponse }); + expect(response).to.be.an('array').that.is.empty; + }); + + it('should return empty array if server response is get error - empty', () => { + const response = spec.interpretResponse({ body: undefined }); + expect(response).to.be.an('array').that.is.empty; + }); + + it('should return correct format, params for each of offers from server response', () => { + const offer = response[0]; + expect(offer.id).to.be.an('string').that.is.not.empty; + expect(offer.impid).to.be.an('string').that.is.not.empty; + expect(offer.requestId).to.be.an('string').that.is.not.empty; + expect(offer.creativeId).to.be.an('string').that.is.not.empty; + expect(offer.netRevenue).to.be.an('boolean'); + expect(offer.ttl).to.be.an('number'); + expect(offer.cpm).to.be.an('number').greaterThan(0); + expect(offer.adserverTargeting).to.be.an('object'); + expect(offer.adserverTargeting['hb_ad_eclickads']).to.be.an('string').that + .is.not.empty; + }); + }); +}); From 7f15127316fe0ca05aa715dada8cf61158cbd2d6 Mon Sep 17 00:00:00 2001 From: MartinGumGum <109325501+MartinGumGum@users.noreply.github.com> Date: Mon, 5 Aug 2024 06:17:45 -0700 Subject: [PATCH 40/51] GumGum Bid Adapter: Send new tpl paramter which is topmostLocation (#12069) * I added a new wl parameter to the payload. * Changed wl parameter name to tpl and pulled the data from topmostLocation --- modules/gumgumBidAdapter.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index ecab3d0b970..7f48a562177 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -29,13 +29,14 @@ let invalidRequestIds = {}; let pageViewId = null; // TODO: potential 0 values for browserParams sent to ad server -function _getBrowserParams(topWindowUrl) { +function _getBrowserParams(topWindowUrl, mosttopLocation) { const paramRegex = paramName => new RegExp(`[?#&](${paramName}=(.*?))($|&)`, 'i'); let browserParams = {}; let topWindow; let topScreen; let topUrl; + let mosttopURL let ggad; let ggdeal; let ns; @@ -74,6 +75,7 @@ function _getBrowserParams(topWindowUrl) { topWindow = global.top; topScreen = topWindow.screen; topUrl = topWindowUrl || ''; + mosttopURL = mosttopLocation || ''; } catch (error) { logError(error); return browserParams; @@ -85,6 +87,7 @@ function _getBrowserParams(topWindowUrl) { sw: topScreen.width, sh: topScreen.height, pu: stripGGParams(topUrl), + tpl: mosttopURL, ce: storage.cookiesAreEnabled(), dpr: topWindow.devicePixelRatio || 1, jcsi: JSON.stringify(JCSI), @@ -304,6 +307,7 @@ function buildRequests(validBidRequests, bidderRequest) { const timeout = bidderRequest && bidderRequest.timeout const coppa = config.getConfig('coppa') === true ? 1 : 0; const topWindowUrl = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page; + const mosttopLocation = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.topmostLocation _each(validBidRequests, bidRequest => { const { bidId, @@ -443,7 +447,7 @@ function buildRequests(validBidRequests, bidderRequest) { sizes, url: BID_ENDPOINT, method: 'GET', - data: Object.assign(data, _getBrowserParams(topWindowUrl)) + data: Object.assign(data, _getBrowserParams(topWindowUrl, mosttopLocation)) }); }); return bids; From b8659d41749193b4a10675509d49d3765b94432a Mon Sep 17 00:00:00 2001 From: Pedro Ribeiro Date: Mon, 5 Aug 2024 09:24:26 -0400 Subject: [PATCH 41/51] Navegg UserID Submodule: conform with pub storage configuration (#12032) * dev/ simple solution * dev: working script and working tests * dev/ final adjustments * dev: minor-changes * hotfix/ importing only the necessary functions --- modules/naveggIdSystem.js | 120 ++++++++-------- modules/naveggIdSystem.md | 8 ++ test/spec/modules/naveggIdSystem_spec.js | 172 +++++++++++++++++++---- 3 files changed, 211 insertions(+), 89 deletions(-) diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 42c6b113566..6f1964b11df 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -6,9 +6,9 @@ */ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import { ajax } from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -19,61 +19,72 @@ const MODULE_NAME = 'naveggId'; const OLD_NAVEGG_ID = 'nid'; const NAVEGG_ID = 'nvggid'; const BASE_URL = 'https://id.navegg.com/uid/'; -const DEFAULT_EXPIRE = 8 * 24 * 3600 * 1000; -const INVALID_EXPIRE = 3600 * 1000; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); -function getNaveggIdFromApi() { - const callbacks = { - success: response => { - if (response) { - try { - const responseObj = JSON.parse(response); - writeCookie(NAVEGG_ID, responseObj[NAVEGG_ID]); - } catch (error) { - logError(error); +function getIdFromAPI() { + const resp = function (callback) { + ajaxBuilder()( + BASE_URL, + response => { + if (response) { + let responseObj; + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); + const fallbackValue = getNaveggIdFromLocalStorage() || getOldCookie(); + callback(fallbackValue); + } + + if (responseObj && responseObj[NAVEGG_ID]) { + callback(responseObj[NAVEGG_ID]); + } else { + const fallbackValue = getNaveggIdFromLocalStorage() || getOldCookie(); + callback(fallbackValue); + } } - } - }, - error: error => { - logError('Navegg ID fetch encountered an error', error); - } + }, + error => { + logError('Navegg ID fetch encountered an error', error); + const fallbackValue = getNaveggIdFromLocalStorage() || getOldCookie(); + callback(fallbackValue); + }, + {method: 'GET', withCredentials: false}); }; - ajax(BASE_URL, callbacks, undefined, { method: 'GET', withCredentials: false }); -} - -function writeCookie(key, value) { - try { - if (storage.cookiesAreEnabled) { - let expTime = new Date(); - const expires = value ? DEFAULT_EXPIRE : INVALID_EXPIRE; - expTime.setTime(expTime.getTime() + expires); - storage.setCookie(key, value, expTime.toUTCString(), 'none'); - } - } catch (e) { - logError(e); - } + return resp; } -function readnaveggIdFromLocalStorage() { - return storage.localStorageIsEnabled ? storage.getDataFromLocalStorage(NAVEGG_ID) : null; +/** + * @returns {string | null} + */ +function readNvgIdFromCookie() { + return storage.cookiesAreEnabled ? (storage.findSimilarCookies('nvg') ? storage.findSimilarCookies('nvg')[0] : null) : null; } - -function readnaveggIDFromCookie() { - return storage.cookiesAreEnabled ? storage.getCookie(NAVEGG_ID) : null; +/** + * @returns {string | null} + */ +function readNavIdFromCookie() { + return storage.cookiesAreEnabled() ? (storage.findSimilarCookies('nav') ? storage.findSimilarCookies('nav')[0] : null) : null; } - -function readoldnaveggIDFromCookie() { - return storage.cookiesAreEnabled ? storage.getCookie(OLD_NAVEGG_ID) : null; +/** + * @returns {string | null} + */ +function readOldNaveggIdFromCookie() { + return storage.cookiesAreEnabled() ? storage.getCookie(OLD_NAVEGG_ID) : null; } - -function readnvgIDFromCookie() { - return storage.cookiesAreEnabled ? (storage.findSimilarCookies('nvg') ? storage.findSimilarCookies('nvg')[0] : null) : null; +/** + * @returns {string | null} + */ +function getOldCookie() { + const oldCookie = readOldNaveggIdFromCookie() || readNvgIdFromCookie() || readNavIdFromCookie(); + return oldCookie; } - -function readnavIDFromCookie() { - return storage.cookiesAreEnabled ? (storage.findSimilarCookies('nav') ? storage.findSimilarCookies('nav')[0] : null) : null; +/** + * @returns {string | null} + */ +function getNaveggIdFromLocalStorage() { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(NAVEGG_ID) : null; } /** @type {Submodule} */ @@ -95,23 +106,16 @@ export const naveggIdSubmodule = { 'naveggId': naveggIdVal.split('|')[0] } : 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(); - - if (typeof naveggIdString == 'string' && naveggIdString) { - try { - return { id: naveggIdString }; - } catch (error) { - logError(error); - } - } - return undefined; + getId(config, consentData) { + const resp = getIdFromAPI() + return {callback: resp} }, eids: { 'naveggId': { diff --git a/modules/naveggIdSystem.md b/modules/naveggIdSystem.md index f758fbc9d5d..81d9841c78f 100644 --- a/modules/naveggIdSystem.md +++ b/modules/naveggIdSystem.md @@ -10,6 +10,11 @@ pbjs.setConfig({ userSync: { userIds: [{ name: 'naveggId', + storage: { + name : 'nvggid', + type : 'cookie&html5', + expires: 8 + } }] } }); @@ -20,3 +25,6 @@ The below parameters apply only to the naveggID integration. | Param under usersync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String | ID of the module - `"naveggId"` | `"naveggId"` | +| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"nvggid"` | +| storage.type | Required | String | Must be "`cookie`", "`html5`" or "`cookie&html5`". This is where the results of the user ID will be stored. | `"cookie&html5"` | +| storage.expires | Required | Integer | How long (in days) the user ID information will be stored. | `8` | diff --git a/test/spec/modules/naveggIdSystem_spec.js b/test/spec/modules/naveggIdSystem_spec.js index 2c4f1cda859..4907a63abde 100644 --- a/test/spec/modules/naveggIdSystem_spec.js +++ b/test/spec/modules/naveggIdSystem_spec.js @@ -1,45 +1,155 @@ -import { naveggIdSubmodule, storage } from 'modules/naveggIdSystem.js'; +import { naveggIdSubmodule, storage, getIdFromAPI } from 'modules/naveggIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import * as ajaxLib from 'src/ajax.js'; -describe('naveggId', function () { - let sandbox; - beforeEach(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(storage, 'getDataFromLocalStorage'); +const NAVEGGID_CONFIG_COOKIE_HTML5 = { + storage: { + name: 'nvggid', + type: 'cookie&html5', + expires: 8 + } +} + +const MOCK_RESPONSE = { + nvggid: 'test_nvggid' +} + +const MOCK_RESPONSE_NULL = { + nvggid: null +} + +function mockResponse(responseText, isSuccess = true) { + return function(url, callbacks) { + if (isSuccess) { + callbacks.success(responseText) + } else { + callbacks.error(new Error('Mock Error')) + } + } +} + +function deleteAllCookies() { + document.cookie.split(';').forEach(cookie => { + const eqPos = cookie.indexOf('='); + const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; + }); +} + +function setLocalStorage() { + storage.setDataInLocalStorage('nvggid', 'localstorage_value'); +} + +describe('getId', function () { + let ajaxStub, ajaxBuilderStub; + + beforeEach(function() { + ajaxStub = sinon.stub(); + ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').returns(ajaxStub); + }); + + afterEach(function() { + ajaxBuilderStub.restore(); + deleteAllCookies(); + storage.removeDataFromLocalStorage('nvggid'); + }); + + it('should get the value from the existing localstorage', function() { + setLocalStorage(); + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; + + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('localstorage_value')).to.be.true; }); - afterEach(() => { - sandbox.restore(); + + it('should get the value from a nid cookie', function() { + storage.setCookie('nid', 'old_nid_cookie', storage.expires) + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; + + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('old_nid_cookie')).to.be.true; }); - it('should NOT find navegg id', function () { - let id = naveggIdSubmodule.getId(); + it('should get the value from a nav cookie', function() { + storage.setCookie('navId', 'old_nav_cookie', storage.expires) + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; - expect(id).to.be.undefined; + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('old_nav_cookie')).to.be.true; }); - it('getId() should return "test-nid" id from cookie OLD_NAVEGG_ID', function() { - sinon.stub(storage, 'getCookie').withArgs('nid').returns('test-nid'); - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'test-nid'}) - }) + it('should get the value from an old nvg cookie', function() { + storage.setCookie('nvgid', 'old_nvg_cookie', storage.expires) + + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; + + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) - it('getId() should return "test-nvggid" id from local storage NAVEGG_ID', function() { - storage.getDataFromLocalStorage.callsFake(() => 'test-ninvggidd') + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('old_nvg_cookie')).to.be.true; + }); - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'test-ninvggidd'}) - }) + it('should return correct value from API response', function(done) { + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; - it('getId() should return "test-nvggid" id from local storage NAV0', function() { - storage.getDataFromLocalStorage.callsFake(() => 'nvgid-nav0') + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE)); + } + }); + apiCallback(callback) - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'nvgid-nav0'}) - }) + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith('test_nvggid')).to.be.true; + done(); + }); - it('getId() should return "test-nvggid" id from local storage NVG0', function() { - storage.getDataFromLocalStorage.callsFake(() => 'nvgid-nvg0') + it('should return no value from API response', function(done) { + const callback = sinon.spy(); + const apiCallback = naveggIdSubmodule.getId(NAVEGGID_CONFIG_COOKIE_HTML5).callback; - let id = naveggIdSubmodule.getId(); - expect(id).to.be.deep.equal({id: 'nvgid-nvg0'}) - }) + ajaxStub.callsFake((url, successCallbacks, errorCallback, options) => { + if (successCallbacks && typeof successCallbacks === 'function') { + successCallbacks(JSON.stringify(MOCK_RESPONSE_NULL)); + } + }); + apiCallback(callback) + expect(callback.calledOnce).to.be.true; + expect(callback.calledWith(undefined)).to.be.true; + done(); + }); }); From 58dd99a929cc12de4495200e3d842b03eaa38791 Mon Sep 17 00:00:00 2001 From: danijel-ristic <168181386+danijel-ristic@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:45:18 +0200 Subject: [PATCH 42/51] Target Video Ad Server Module: initial release (#11761) * Add target video ad server video support * Fix url decoding * Update server url * Update server subdomain * Add cust_params merging, add all targeting data * fix cust_params append function * Update ad server video module --------- Co-authored-by: Danijel Ristic --- modules/targetVideoAdServerVideo.js | 96 ++++++++++ modules/targetVideoAdServerVideo.md | 26 +++ .../modules/targetVideoAdServerVideo_spec.js | 179 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 modules/targetVideoAdServerVideo.js create mode 100644 modules/targetVideoAdServerVideo.md create mode 100644 test/spec/modules/targetVideoAdServerVideo_spec.js diff --git a/modules/targetVideoAdServerVideo.js b/modules/targetVideoAdServerVideo.js new file mode 100644 index 00000000000..e54aa1ab141 --- /dev/null +++ b/modules/targetVideoAdServerVideo.js @@ -0,0 +1,96 @@ +import { registerVideoSupport } from '../src/adServerManager.js'; +import { targeting } from '../src/targeting.js'; +import { buildUrl, isEmpty, isPlainObject, logError, parseUrl } from '../src/utils.js'; + +/** + * Merge all the bid data and publisher-supplied options into a single URL, and then return it. + * @param {Object} options - The options object. + * @param {Object} options.params - params property. + * @param {string} options.params.iu - Required iu property. + * @param {Object} options.adUnit - adUnit property. + * @param {Object} [options.bid] - Optional bid property. + * @returns {string|undefined} The generated URL (if neither bid nor adUnit property is provided the generated string is undefined). + */ +export function buildVideoUrl(options) { + if (!options.params || !options.params?.iu) { + logError('buildVideoUrl: Missing required properties.'); + return; + } + + if (!isPlainObject(options.adUnit) && !isPlainObject(options.bid)) { + logError('buildVideoUrl: Requires either \'adUnit\' or \'bid\' options value'); + return; + } + + const isURL = /^(https?:\/\/)/i; + const defaultParameters = { + autoplay: '[autoplay]', + mute: '[vpmute]', + page_url: '[page_url]', + cachebuster: '[timestamp]', + consent: '[consent]', + } + + const adUnit = options.adUnit; + const bid = options.bid || targeting.getWinningBids(adUnit.code)[0]; + const allTargetingData = getAllTargetingData(options); + const custParams = options.params.cust_params; + let iu = options.params.iu; + + if (isURL.test(iu)) { + const urlComponents = parseUrl(iu, {noDecodeWholeURL: true}); + + for (const [key, value] of Object.entries({...allTargetingData, ...bid.adserverTargeting, ...defaultParameters})) { + if (!urlComponents.search.hasOwnProperty(key)) { + urlComponents.search[key] = value; + } + } + + if (urlComponents.search.cust_params) { + for (const [key, value] of Object.entries(custParams)) { + if (!urlComponents.search.cust_params.includes(key)) { + urlComponents.search.cust_params += '%26' + key + '%3D' + value; + } + } + } + + if (!isEmpty(custParams) && !urlComponents.search.cust_params) { + urlComponents.search.cust_params = Object.entries(custParams).map(([key, value]) => key + '%3D' + value).join('%26'); + } + + return buildUrl(urlComponents); + } + + const search = { + iu, + ...defaultParameters, + ...allTargetingData, + ...bid.adserverTargeting, + }; + + if (!isEmpty(custParams)) { + search.cust_params = Object.entries(custParams).map(([key, value]) => key + '%3D' + value).join('%26'); + } + + return buildUrl({ + protocol: 'https', + host: 'vid.tvserve.io', + pathname: '/ads/bid', + search + }); +} + +function getAllTargetingData(options) { + let allTargetingData = {}; + const adUnit = options && options.adUnit; + if (adUnit) { + let allTargeting = targeting.getAllTargeting(adUnit.code); + allTargetingData = allTargeting ? allTargeting[adUnit.code] : {}; + } + + return allTargetingData; +} + +registerVideoSupport('targetVideo', { + buildVideoUrl, +}); diff --git a/modules/targetVideoAdServerVideo.md b/modules/targetVideoAdServerVideo.md new file mode 100644 index 00000000000..89592b32478 --- /dev/null +++ b/modules/targetVideoAdServerVideo.md @@ -0,0 +1,26 @@ +# Overview + +``` +Module Name: Target Video Ad Server Video +Module Type: Ad Server Video +Maintainer: danijel.ristic@target-video.com +``` + +# Description + +Ad Server Video for target-video.com. Contact danijel.ristic@target-video.com for information. + +# Integration + +The helper function takes the form: + + pbjs.adServers.targetVideo.buildVideoUrl(options) + +Where: + +* **`options`:** configuration object: + * **`params`:** + * **`iu`:** required property used to construct valid VAST tag URL + * **`adUnit`:** ad unit that is being filled + * **`bid` [optional]:** if you override the hardcoded `pbjs.adServers.dfp.buildVideoUrl(...)` logic that picks the first bid you *must* pass in the `bid` object you select + * **`url`:** VAST tag URL, similar to the value returned by `pbjs.adServers.dfp.buildVideoUrl(...)` diff --git a/test/spec/modules/targetVideoAdServerVideo_spec.js b/test/spec/modules/targetVideoAdServerVideo_spec.js new file mode 100644 index 00000000000..a254d76f2c9 --- /dev/null +++ b/test/spec/modules/targetVideoAdServerVideo_spec.js @@ -0,0 +1,179 @@ +import {expect} from 'chai'; +import {buildVideoUrl} from 'modules/targetVideoAdServerVideo.js'; +import {targeting} from 'src/targeting.js'; +import * as utils from 'src/utils.js'; +import {hook} from '../../../src/hook.js'; +import AD_UNIT from 'test/fixtures/video/adUnit.json'; + +describe('TargetVideo Ad Server Video', function() { + before(() => { + hook.ready(); + }); + + let sandbox, bid, adUnit; + const unitUrl = { iu: 'https://example.com/ads/bid?iu=/video' }; + const unitId = { iu: '/video' }; + const allTargeting = { + 'hb_format': 'video', + 'hb_source': 'client', + 'hb_size': '640x480', + 'hb_pb': '5.00', + 'hb_adid': '2c4f6cc3ba128a', + 'hb_bidder': 'testBidder2', + 'hb_format_testBidder2': 'video', + 'hb_source_testBidder2': 'client', + 'hb_size_testBidder2': '640x480', + 'hb_pb_testBidder2': '5.00', + 'hb_adid_testBidder2': '2c4f6cc3ba128a', + 'hb_bidder_testBidder2': 'testBidder2', + 'hb_format_targetVideo': 'video', + 'hb_source_targetVideo': 'client', + 'hb_size_targetVideo': '640x480', + 'hb_pb_targetVideo': '5.00', + 'hb_adid_targetVideo': '44e0b5f2e5cace', + 'hb_bidder_targetVideo': 'targetVideo' + }; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + bid = { + videoCacheKey: '123', + adserverTargeting: { + hb_adid: 'ad_id', + hb_cache_id: '123', + hb_uuid: '123', + }, + }; + adUnit = utils.deepClone(AD_UNIT); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return undefined if required properties are missing', () => { + const url1 = buildVideoUrl({ params: {} }); + const url2 = buildVideoUrl({ adUnit: {} }); + const url3 = buildVideoUrl({ bid: {} }); + + expect(url1).to.be.undefined; + expect(url2).to.be.undefined; + expect(url3).to.be.undefined; + }); + + it('should require options.adUnit or options.bid', () => { + const options = { + params: { ...unitUrl } + }; + + const getWinningBidsStub = sandbox.stub(targeting, 'getWinningBids').returns([bid]); + const getAllTargetingDataStub = sandbox.stub(targeting, 'getAllTargeting').returns(allTargeting); + + const url1 = buildVideoUrl(options); + expect(url1).to.be.undefined; + + const url2 = buildVideoUrl(Object.assign(options, { adUnit })); + expect(url2).to.be.string; + + const url3 = buildVideoUrl(Object.assign(options, { bid })); + expect(url3).to.be.string; + + getWinningBidsStub.restore(); + getAllTargetingDataStub.restore(); + }); + + it('should build URL correctly with valid parameters', () => { + const optionsUrl = { + params: { ...unitUrl } + }; + + const optionsId = { + params: { ...unitId } + }; + + const getWinningBidsStub = sandbox.stub(targeting, 'getWinningBids').returns([bid]); + const getAllTargetingDataStub = sandbox.stub(targeting, 'getAllTargeting').returns(allTargeting); + + const url1 = buildVideoUrl(Object.assign(optionsUrl, { bid, adUnit })); + const url2 = buildVideoUrl(Object.assign(optionsId, { bid, adUnit })); + + expect(url1).to.include('https://example.com/ads/bid?iu=/video'); + expect(url1).to.include('hb_adid=ad_id'); + expect(url1).to.include('hb_cache_id=123'); + expect(url1).to.include('hb_uuid=123'); + + expect(url2).to.include('https://vid.tvserve.io/ads/bid?iu=/video'); + expect(url2).to.include('hb_adid=ad_id'); + expect(url2).to.include('hb_cache_id=123'); + expect(url2).to.include('hb_uuid=123'); + + getWinningBidsStub.restore(); + getAllTargetingDataStub.restore(); + }); + + it('should include default query parameters', () => { + const options = { + params: { ...unitUrl } + }; + + const getWinningBidsStub = sandbox.stub(targeting, 'getWinningBids').returns([bid]); + const getAllTargetingDataStub = sandbox.stub(targeting, 'getAllTargeting').returns(allTargeting); + + const url = buildVideoUrl(Object.assign(options, { bid, adUnit })); + + expect(url).to.include('autoplay=[autoplay]'); + expect(url).to.include('mute=[vpmute]'); + expect(url).to.include('page_url=[page_url]'); + expect(url).to.include('cachebuster=[timestamp]'); + expect(url).to.include('consent=[consent]'); + + getWinningBidsStub.restore(); + getAllTargetingDataStub.restore(); + }); + + it('should add cust_params correctly', () => { + const optionsUrl = { + params: { + ...unitUrl, + cust_params: { + targeting_1: 'foo', + targeting_2: 'bar' + } + } + }; + + const optionsId = { + params: { + ...unitId, + cust_params: { + targeting_1: 'foo', + targeting_2: 'bar' + } + } + }; + + const optionsToMergeCustParams = { + params: { + iu: 'https://example.com/ads/bid?iu=/video&cust_params=targeting_1%3Dbaz', + cust_params: { + targeting_1: 'foo', + targeting_2: 'bar' + } + } + }; + + const getWinningBidsStub = sandbox.stub(targeting, 'getWinningBids').returns([bid]); + const getAllTargetingDataStub = sandbox.stub(targeting, 'getAllTargeting').returns(allTargeting); + + const url1 = buildVideoUrl(Object.assign(optionsUrl, { bid, adUnit })); + const url2 = buildVideoUrl(Object.assign(optionsId, { bid, adUnit })); + const url3 = buildVideoUrl(Object.assign(optionsToMergeCustParams, { bid, adUnit })); + + expect(url1).to.include('cust_params=targeting_1%3Dfoo%26targeting_2%3Dbar'); + expect(url2).to.include('cust_params=targeting_1%3Dfoo%26targeting_2%3Dbar'); + expect(url3).to.include('cust_params=targeting_1%3Dbaz%26targeting_2%3Dbar'); + + getWinningBidsStub.restore(); + getAllTargetingDataStub.restore(); + }); +}); From e10a58c0c5abbf31141fade29fa67dea77f4b03d Mon Sep 17 00:00:00 2001 From: mkomorski Date: Mon, 5 Aug 2024 15:52:36 +0200 Subject: [PATCH 43/51] #9573 adding onAddRenderSucceeded to bidder spec (#11998) Co-authored-by: Marcin Komorski --- src/adRendering.js | 3 +++ src/adapterManager.js | 4 ++++ test/spec/unit/adRendering_spec.js | 28 +++++++++++++++++++++- test/spec/unit/core/adapterManager_spec.js | 10 ++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/adRendering.js b/src/adRendering.js index 9eb894ec190..e4acfd2d69a 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -18,6 +18,7 @@ import {getCreativeRenderer} from './creativeRenderers.js'; import {hook} from './hook.js'; import {fireNativeTrackers} from './native.js'; import {GreedyPromise} from './utils/promise.js'; +import adapterManager from './adapterManager.js'; const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON } = EVENTS; const { EXCEPTION } = AD_RENDER_FAILED_REASON; @@ -68,6 +69,8 @@ export function emitAdRenderSucceeded({ doc, bid, id }) { if (bid) data.bid = bid; if (id) data.adId = id; + adapterManager.callAdRenderSucceededBidder(bid.adapterCode || bid.bidder, bid); + events.emit(AD_RENDER_SUCCEEDED, data); } diff --git a/src/adapterManager.js b/src/adapterManager.js index 2d4bd7a5f6b..a571ba62ddc 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -688,6 +688,10 @@ adapterManager.callBidderError = function(bidder, error, bidderRequest) { tryCallBidderMethod(bidder, 'onBidderError', param); }; +adapterManager.callAdRenderSucceededBidder = function (bidder, bid) { + tryCallBidderMethod(bidder, 'onAdRenderSucceeded', bid); +} + function resolveAlias(alias) { const seen = new Set(); while (_aliasRegistry.hasOwnProperty(alias) && !seen.has(alias)) { diff --git a/test/spec/unit/adRendering_spec.js b/test/spec/unit/adRendering_spec.js index 4d0962a0b2c..971e92224e9 100644 --- a/test/spec/unit/adRendering_spec.js +++ b/test/spec/unit/adRendering_spec.js @@ -1,7 +1,7 @@ import * as events from 'src/events.js'; import * as utils from 'src/utils.js'; import { - doRender, getBidToRender, + doRender, emitAdRenderSucceeded, getBidToRender, getRenderingData, handleCreativeEvent, handleNativeMessage, @@ -12,6 +12,7 @@ import {expect} from 'chai/index.mjs'; import {config} from 'src/config.js'; import {VIDEO} from '../../../src/mediaTypes.js'; import {auctionManager} from '../../../src/auctionManager.js'; +import adapterManager from '../../../src/adapterManager.js'; describe('adRendering', () => { let sandbox; @@ -267,4 +268,29 @@ describe('adRendering', () => { sinon.assert.calledWith(fireTrackers, data, bid); }) }) + + describe('onAdRenderSucceeded', () => { + let mockAdapterSpec, bids; + beforeEach(() => { + mockAdapterSpec = { + onAdRenderSucceeded: sinon.stub() + }; + adapterManager.bidderRegistry['mockBidder'] = { + bidder: 'mockBidder', + getSpec: function () { return mockAdapterSpec; }, + }; + bids = [ + { bidder: 'mockBidder', params: { placementId: 'id' } }, + ]; + }); + + afterEach(function () { + delete adapterManager.bidderRegistry['mockBidder']; + }); + + it('should invoke onAddRenderSucceeded on emitAdRenderSucceeded', () => { + emitAdRenderSucceeded({ bid: bids[0] }); + sinon.assert.called(mockAdapterSpec.onAdRenderSucceeded); + }); + }); }); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 9132aaeb903..a68634501ac 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -428,6 +428,16 @@ describe('adapterManager tests', function () { sinon.assert.notCalled(criteoSpec.onBidViewable); }) }); + + describe('onAdRenderSucceeded', function () { + beforeEach(() => { + criteoSpec.onAdRenderSucceeded = sinon.stub() + }); + it('should call spec\'s onAdRenderSucceeded callback', function () { + adapterManager.callAdRenderSucceededBidder(bids[0].bidder, bids[0]); + sinon.assert.called(criteoSpec.onAdRenderSucceeded); + }); + }); }) describe('onBidderError', function () { const bidder = 'appnexus'; From 81459cd6374a582b6377599196507a5b38a169ce Mon Sep 17 00:00:00 2001 From: Jonathan Nadarajah <50102657+jogury@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:23:19 +0200 Subject: [PATCH 44/51] Ogury Adapter add gpid in bid request (#12091) --- modules/oguryBidAdapter.js | 7 ++- test/spec/modules/oguryBidAdapter_spec.js | 54 +++++++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 3cf78da4e3a..0dd07a26e2c 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -1,7 +1,7 @@ 'use strict'; import {BANNER} from '../src/mediaTypes.js'; -import {getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; +import {getWindowSelf, getWindowTop, isFn, logWarn, deepAccess} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ajax} from '../src/ajax.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; @@ -12,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.6.0'; +const ADAPTER_VERSION = '1.6.1'; function getClientWidth() { const documentElementClientWidth = window.top.document.documentElement.clientWidth @@ -129,6 +129,8 @@ function buildRequests(validBidRequests, bidderRequest) { openRtbBidRequestBanner.user.ext.eids = bidRequest.userIdAsEids } + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + openRtbBidRequestBanner.imp.push({ id: bidRequest.bidId, tagid: bidRequest.params.adUnitId, @@ -138,6 +140,7 @@ function buildRequests(validBidRequests, bidderRequest) { }, ext: { ...bidRequest.params, + ...(gpid && {gpid}), timeSpentOnPage: document.timeline && document.timeline.currentTime ? document.timeline.currentTime : 0 } }); diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index ea923a18e57..861c50b1a98 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -13,6 +13,11 @@ describe('OguryBidAdapter', function () { bidRequests = [ { adUnitCode: 'adUnitCode', + ortb2Imp: { + ext: { + gpid: 'gpid' + } + }, auctionId: 'auctionId', bidId: 'bidId', bidder: 'ogury', @@ -394,6 +399,7 @@ describe('OguryBidAdapter', function () { }, ext: { ...bidRequests[0].params, + gpid: bidRequests[0].ortb2Imp.ext.gpid, timeSpentOnPage: stubbedCurrentTime } }, { @@ -441,7 +447,7 @@ describe('OguryBidAdapter', function () { }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.6.0' + adapterversion: '1.6.1' }, device: { w: stubbedWidth, @@ -824,7 +830,47 @@ describe('OguryBidAdapter', function () { const request = spec.buildRequests(validBidRequests, bidderRequest); expect(request.data).to.deep.equal(expectedRequestWithUnsupportedFloorCurrency); }); - }); + + it('should not add gpid if ortb2 undefined', () => { + const expectedRequestWithUndefinedGpid = utils.deepClone(expectedRequestObject) + + delete expectedRequestWithUndefinedGpid.imp[0].ext.gpid; + delete expectedRequestWithUndefinedGpid.imp[1].ext.gpid; + + const validBidRequests = utils.deepClone(bidRequests); + delete validBidRequests[0].ortb2Imp.ext.gpid; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedGpid); + }); + + it('should not add gpid if gpid undefined', () => { + const expectedRequestWithUndefinedGpid = utils.deepClone(expectedRequestObject) + + delete expectedRequestWithUndefinedGpid.imp[0].ext.gpid; + delete expectedRequestWithUndefinedGpid.imp[1].ext.gpid; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + ortb2Imp: { + ext: {} + } + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedGpid); + }); + + it('should send gpid in bid request', function() { + const validBidRequests = utils.deepClone(bidRequests) + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestObject); + expect(request.data.imp[0].ext.gpid).to.be.a('string'); + expect(request.data.imp[1].ext.gpid).to.be.undefined + }) + }) describe('interpretResponse', function () { let openRtbBidResponse = { @@ -891,7 +937,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.6.0', + adapterVersion: '1.6.1', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -908,7 +954,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.6.0', + adapterVersion: '1.6.1', prebidVersion: '$prebid.version$' }] From 22d913e59e75f01ac7a3b35cfd7989db3e1c2136 Mon Sep 17 00:00:00 2001 From: MykhailoTeqBlaze Date: Wed, 7 Aug 2024 16:04:21 +0300 Subject: [PATCH 45/51] fix isBidRequestValid() (#12093) --- libraries/teqblazeUtils/bidderUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/teqblazeUtils/bidderUtils.js b/libraries/teqblazeUtils/bidderUtils.js index ce061ac7f3c..96195eed70d 100644 --- a/libraries/teqblazeUtils/bidderUtils.js +++ b/libraries/teqblazeUtils/bidderUtils.js @@ -102,7 +102,7 @@ const checkIfObjectHasKey = (keys, obj, mode = 'some') => { const val = obj[key]; if (mode === 'some' && val) return true; - if (!val) return false; + if (mode === 'every' && !val) return false; } return mode === 'every'; From 256556202a284065da1ac235391882ef4def7032 Mon Sep 17 00:00:00 2001 From: Brian Schmidt Date: Thu, 8 Aug 2024 05:01:09 -0700 Subject: [PATCH 46/51] openxBidAdapter remove PAF, bugfix ortbConverter response (#12105) --- modules/openxBidAdapter.js | 7 +-- test/spec/modules/openxBidAdapter_spec.js | 63 ++++++----------------- 2 files changed, 16 insertions(+), 54 deletions(-) diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 8b16aa1a84e..19da19e661f 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -80,11 +80,6 @@ const converter = ortbConverter({ bidResponse.meta.advertiserId = bid.ext.buyer_id; bidResponse.meta.brandId = bid.ext.brand_id; } - const {ortbResponse} = context; - if (ortbResponse.ext && ortbResponse.ext.paf) { - bidResponse.meta.paf = Object.assign({}, ortbResponse.ext.paf); - bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); - } return bidResponse; }, response(buildResponse, bidResponses, ortbResponse, context) { @@ -117,7 +112,7 @@ const converter = ortbConverter({ paapi: fledgeAuctionConfigs, } } else { - return response.bids + return response } }, overrides: { diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index e895574b9aa..dbc036c860b 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1109,7 +1109,7 @@ describe('OpenxRtbAdapter', function () { let bid; context('when there is an nbr response', function () { - let bids; + let response; beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1131,16 +1131,16 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = {nbr: 0}; // Unknown error - bids = spec.interpretResponse({body: bidResponse}, bidRequest); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); it('should not return any bids', function () { - expect(bids.length).to.equal(0); + expect(response.bids.length).to.equal(0); }); }); context('when no seatbid in response', function () { - let bids; + let response; beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1162,16 +1162,16 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = {ext: {}, id: 'test-bid-id'}; - bids = spec.interpretResponse({body: bidResponse}, bidRequest); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); it('should not return any bids', function () { - expect(bids.length).to.equal(0); + expect(response.bids.length).to.equal(0); }); }); context('when there is no response', function () { - let bids; + let response; beforeEach(function () { bidRequestConfigs = [{ bidder: 'openx', @@ -1193,11 +1193,11 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = ''; // Unknown error - bids = spec.interpretResponse({body: bidResponse}, bidRequest); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); it('should not return any bids', function () { - expect(bids.length).to.equal(0); + expect(response.bids.length).to.equal(0); }); }); @@ -1232,19 +1232,11 @@ describe('OpenxRtbAdapter', function () { ext: { dsp_id: '123', buyer_id: '456', - brand_id: '789', - paf: { - content_id: 'paf_content_id' - } + brand_id: '789' } }] }], - cur: 'AUS', - ext: { - paf: { - transmission: {version: '12'} - } - } + cur: 'AUS' }; context('when there is a response, the common response properties', function () { @@ -1253,7 +1245,7 @@ describe('OpenxRtbAdapter', function () { bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; bidResponse = deepClone(SAMPLE_BID_RESPONSE); - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; }); it('should return a price', function () { @@ -1308,33 +1300,8 @@ describe('OpenxRtbAdapter', function () { it('should return adomain', function () { expect(bid.meta.advertiserDomains).to.equal(bidResponse.seatbid[0].bid[0].adomain); }); - - it('should return paf fields', function () { - const paf = { - transmission: {version: '12'}, - content_id: 'paf_content_id' - } - expect(bid.meta.paf).to.deep.equal(paf); - }); }); - context('when there is more than one response', () => { - let bids; - beforeEach(function () { - bidRequestConfigs = deepClone(SAMPLE_BID_REQUESTS); - bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; - bidResponse = deepClone(SAMPLE_BID_RESPONSE); - bidResponse.seatbid[0].bid.push(deepClone(bidResponse.seatbid[0].bid[0])); - bidResponse.seatbid[0].bid[1].ext.paf.content_id = 'second_paf' - - bids = spec.interpretResponse({body: bidResponse}, bidRequest); - }); - - it('should not confuse paf content_id', () => { - expect(bids.map(b => b.meta.paf.content_id)).to.eql(['paf_content_id', 'second_paf']); - }); - }) - context('when the response is a banner', function() { beforeEach(function () { bidRequestConfigs = [{ @@ -1371,7 +1338,7 @@ describe('OpenxRtbAdapter', function () { cur: 'AUS' }; - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; }); it('should return the proper mediaType', function () { @@ -1420,14 +1387,14 @@ describe('OpenxRtbAdapter', function () { }); it('should return the proper mediaType', function () { - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); }); it('should return the proper mediaType', function () { const winUrl = 'https//my.win.url'; bidResponse.seatbid[0].bid[0].nurl = winUrl - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + bid = spec.interpretResponse({body: bidResponse}, bidRequest).bids[0]; expect(bid.vastUrl).to.equal(winUrl); }); From 6998a3d7f52de38117e80f3a14ecd8300f16e428 Mon Sep 17 00:00:00 2001 From: Dmitry Sinev Date: Thu, 8 Aug 2024 15:04:14 +0300 Subject: [PATCH 47/51] Appnexus Bid Adapter: fix parse of the encoded string to check for ast_override_div argument (#12106) --- modules/appnexusBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 387df07a8b4..4935158d21c 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -747,7 +747,7 @@ function bidToTag(bid) { // page.html?ast_override_div=divId:creativeId,divId2:creativeId2 const overrides = getParameterByName('ast_override_div'); if (isStr(overrides) && overrides !== '') { - const adUnitOverride = overrides.split(',').find((pair) => pair.startsWith(`${bid.adUnitCode}:`)); + const adUnitOverride = decodeURIComponent(overrides).split(',').find((pair) => pair.startsWith(`${bid.adUnitCode}:`)); if (adUnitOverride) { const forceCreativeId = adUnitOverride.split(':')[1]; if (forceCreativeId) { From 439d6edf08860da91fe6f4b2ab4427be31ff0ce7 Mon Sep 17 00:00:00 2001 From: Rich Audience Date: Thu, 8 Aug 2024 14:07:41 +0200 Subject: [PATCH 48/51] Richaudience Bid Adapter : add compability with DSA (#12099) * Update richaudienceBidAdapter.md Update maintainer e-mail to integrations@richaudience.com * Add richaudienceBidAdapter.js file * Add richaudienceBidAdapter_spec.js * Update richaudienceBidAdapter.js * RichaudienceBidAdapter add compability with DSA * RichaudienceBidAdapter add compability with DSA * RichaudienceBidAdapter add compability with DSA --------- Co-authored-by: Patrick McCann Co-authored-by: sergigimenez --- modules/richaudienceBidAdapter.js | 9 ++- .../modules/richaudienceBidAdapter_spec.js | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index c6a3a5427bd..776b855dfba 100644 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -55,7 +55,9 @@ export const spec = { cpuc: (typeof window.navigator != 'undefined' ? window.navigator.hardwareConcurrency : null), kws: getAllOrtbKeywords(bidderRequest.ortb2, bid.params.keywords).join(','), schain: bid.schain, - gpid: raiSetPbAdSlot(bid) + gpid: raiSetPbAdSlot(bid), + dsa: setDSA(bid), + userData: deepAccess(bid, 'ortb2.user.data') }; REFERER = (typeof bidderRequest.refererInfo.page != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.page) : null) @@ -373,3 +375,8 @@ function raiGetTimeoutURL(data) { } return url } + +function setDSA(bid) { + let dsa = bid?.ortb2?.regs?.ext?.dsa ? bid?.ortb2?.regs?.ext?.dsa : null; + return dsa; +} diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index d2b173f53df..782955103b3 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -35,6 +35,53 @@ describe('Richaudience adapter tests', function () { user: {} }]; + var DEFAULT_PARAMS_NEW_DSA = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + banner: { + sizes: [ + [300, 250], [300, 600], [728, 90], [970, 250]] + } + }, + bidder: 'richaudience', + params: { + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site', + keywords: 'key1=value1;key2=value2' + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: 2, + pubrender: 1, + datatopub: 1, + transparency: [ + { + domain: 'richaudience.com', + dsaparams: [1, 3, 6] + }, + { + domain: 'adpone.com', + dsaparams: [8, 10, 12] + }, + { + domain: 'sunmedia.com', + dsaparams: [14, 16, 18] + } + ] + } + } + } + }, + user: {} + }]; + var DEFAULT_PARAMS_NEW_SIZES_GPID = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', @@ -893,6 +940,21 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('schain').to.deep.equal(schain); }) + it('should pass DSA', function() { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_DSA, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: {} + }) + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('dsa').property('dsarequired').and.to.equal(2) + expect(requestContent).to.have.property('dsa').property('pubrender').and.to.equal(1); + expect(requestContent).to.have.property('dsa').property('datatopub').and.to.equal(1); + expect(requestContent.dsa.transparency[0]).to.have.property('domain').and.to.equal('richaudience.com'); + }) + it('should pass gpid', function() { const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES_GPID, { gdprConsent: { From 5f7e86eaa0f94937444a787e510e292c3e07071f Mon Sep 17 00:00:00 2001 From: Alexander Pykhteyev Date: Thu, 8 Aug 2024 20:20:52 +0700 Subject: [PATCH 49/51] Add new TGM adapter (#12100) Co-authored-by: apykhteyev --- modules/limelightDigitalBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index 6c589f536c2..fc5d608f133 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -32,7 +32,7 @@ function isBidResponseValid(bid) { export const spec = { code: BIDDER_CODE, - aliases: ['pll', 'iionads', 'apester', 'adsyield'], + aliases: ['pll', 'iionads', 'apester', 'adsyield', 'tgm'], supportedMediaTypes: [BANNER, VIDEO], /** From c0048a34ae3caadd04cc54e80d9260a883aaded9 Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 8 Aug 2024 20:00:44 +0000 Subject: [PATCH 50/51] Prebid 9.9.0 release --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21ceda42b53..2a1040da177 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "9.9.0-pre", + "version": "9.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "9.9.0-pre", + "version": "9.9.0", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.24.6", diff --git a/package.json b/package.json index 7502dc8794e..ae5c36806fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "9.9.0-pre", + "version": "9.9.0", "description": "Header Bidding Management Library", "main": "src/prebid.public.js", "exports": { From 6b96cba91867304b7c344d786e7eb3d4d76aa5da Mon Sep 17 00:00:00 2001 From: "Prebid.js automated release" Date: Thu, 8 Aug 2024 20:00:44 +0000 Subject: [PATCH 51/51] Increment version to 9.10.0-pre --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a1040da177..e2945d258a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "9.9.0", + "version": "9.10.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "9.9.0", + "version": "9.10.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.24.6", diff --git a/package.json b/package.json index ae5c36806fd..95b24e00427 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "9.9.0", + "version": "9.10.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.public.js", "exports": {