diff --git a/README.md b/README.md index 11dc01e8f07..bbc0d79ab41 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ [![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") -[![Code Climate](https://codeclimate.com/github/prebid/Prebid.js/badges/gpa.svg)](https://codeclimate.com/github/prebid/Prebid.js) [![Coverage Status](https://coveralls.io/repos/github/prebid/Prebid.js/badge.svg)](https://coveralls.io/github/prebid/Prebid.js) -[![devDependencies Status](https://david-dm.org/prebid/Prebid.js/dev-status.svg)](https://david-dm.org/prebid/Prebid.js?type=dev) [![Total Alerts](https://img.shields.io/lgtm/alerts/g/prebid/Prebid.js.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prebid/Prebid.js/alerts/) # Prebid.js diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index e95f83d2c7b..b19c8318754 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,6 +1,6 @@ // ADRIVER BID ADAPTER for Prebid 1.13 import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adriver'; @@ -22,8 +22,6 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - logInfo('validBidRequests', validBidRequests); - let win = getWindowLocation(); let customID = Math.round(Math.random() * 999999999) + '-' + Math.round(new Date() / 1000) + '-1-46-'; let siteId = getBidIdParameter('siteid', validBidRequests[0].params) + ''; @@ -99,22 +97,17 @@ export const spec = { }); }); - let userid = validBidRequests[0].userId; - let adrcidCookie = storage.getDataFromLocalStorage('adrcid') || validBidRequests[0].userId.adrcid; - + let adrcidCookie = storage.getDataFromLocalStorage('adrcid') || validBidRequests[0].userId?.adrcid; if (adrcidCookie) { - payload.adrcid = adrcidCookie; - payload.id5 = userid.id5id; - payload.sharedid = userid.pubcid; - payload.unifiedid = userid.tdid; + payload.user.buyerid = adrcidCookie; } const payloadString = JSON.stringify(payload); return { method: 'POST', url: ADRIVER_BID_URL, - data: payloadString, - }; + data: payloadString + } }, interpretResponse: function (serverResponse, bidRequest) { diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index 6a492fac508..fb8ce99ec16 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -73,7 +73,8 @@ export const adriverIdSubmodule = { callback(); } }; - ajax(url, callbacks, undefined, {method: 'GET'}); + let newUrl = url + '&cid=' + (storage.getDataFromLocalStorage('adrcid') || storage.getCookie('adrcid')); + ajax(newUrl, callbacks, undefined, {method: 'GET'}); } }; return {callback: resp}; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 2ee5b0f72a3..3dad2e98bcf 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -23,6 +23,7 @@ const HOST_GETTERS = { janet: () => 'ghb.bidder.jmgads.com', pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', + vidcrunchllc: () => 'ghb.platform.vidcrunch.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -43,6 +44,7 @@ export const spec = { { code: 'navelix', gvlid: 380 }, 'pgam', 'ocm', + { code: 'vidcrunchllc', gvlid: 1145 }, ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 1c269b2bba4..98aa58f5a70 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -469,15 +469,20 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (bidRequest.params.publisherSubId) { slot.publishersubid = bidRequest.params.publisherSubId; } - if (bidRequest.params.nativeCallback || deepAccess(bidRequest, `mediaTypes.${NATIVE}`)) { + + if (bidRequest.params.nativeCallback || hasNativeMediaType(bidRequest)) { slot.native = true; if (!checkNativeSendId(bidRequest)) { logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); } - slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseNativeSize); - } else { + } + + if (hasBannerMediaType(bidRequest)) { slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseSize); + } else { + slot.sizes = []; } + if (hasVideoMediaType(bidRequest)) { const video = { playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), @@ -554,17 +559,18 @@ function parseSize(size) { return size[0] + 'x' + size[1]; } -function parseNativeSize(size) { - if (size[0] === undefined && size[1] === undefined) { - return '2x2'; - } - return size[0] + 'x' + size[1]; -} - function hasVideoMediaType(bidRequest) { return deepAccess(bidRequest, 'mediaTypes.video') !== undefined; } +function hasBannerMediaType(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.banner') !== undefined; +} + +function hasNativeMediaType(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.native') !== undefined; +} + function hasValidVideoMediaType(bidRequest) { let isValid = true; @@ -646,18 +652,18 @@ function enrichSlotWithFloors(slot, bidRequest) { if (bidRequest.mediaTypes?.banner) { slotFloors.banner = {}; const bannerSizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')) - bannerSizes.forEach(bannerSize => slotFloors.banner[parseSize(bannerSize).toString()] = bidRequest.getFloor({size: bannerSize, mediaType: BANNER})); + bannerSizes.forEach(bannerSize => slotFloors.banner[parseSize(bannerSize).toString()] = bidRequest.getFloor({ size: bannerSize, mediaType: BANNER })); } if (bidRequest.mediaTypes?.video) { slotFloors.video = {}; const videoSizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')) - videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({size: videoSize, mediaType: VIDEO})); + videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({ size: videoSize, mediaType: VIDEO })); } if (bidRequest.mediaTypes?.native) { slotFloors.native = {}; - slotFloors.native['*'] = bidRequest.getFloor({size: '*', mediaType: NATIVE}); + slotFloors.native['*'] = bidRequest.getFloor({ size: '*', mediaType: NATIVE }); } if (Object.keys(slotFloors).length > 0) { diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index cea33014144..e2a29375f25 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -25,6 +25,15 @@ export function getDgKeywordsAndSet(reqBidsConfigObj, callback, moduleConfig, us const timeout = (moduleConfig && moduleConfig.params && moduleConfig.params.timeout && Number(moduleConfig.params.timeout) > 0) ? Number(moduleConfig.params.timeout) : PROFILE_TIMEOUT_MS; const url = (moduleConfig && moduleConfig.params && moduleConfig.params.url) ? moduleConfig.params.url : URL + encodeURIComponent(window.location.href); const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + callback = (function(cb) { + let done = false; + return function () { + if (!done) { + done = true; + return cb.apply(this, arguments); + } + } + })(callback); let isFinish = false; logMessage('[dgkeyword sub module]', adUnits, timeout); let setKeywordTargetBidders = getTargetBidderOfDgKeywords(adUnits); diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index e039d461fc7..c3c6597dd1b 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,16 +1,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {getWindowFromDocument, logWarn} from '../src/utils.js'; -const BIDDER_VERSION = '1.0.0'; +const ADAPTER_VERSION = '1.1.0'; const BIDDER_CODE = 'displayio'; -const GVLID = 999; const BID_TTL = 300; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const DEFAULT_CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, - gvlid: GVLID, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function(bid) { return !!(bid.params && bid.params.placementId && bid.params.siteId && @@ -20,7 +20,7 @@ export const spec = { return bidRequests.map(bid => { let url = '//' + bid.params.adsSrvDomain + '/srv?method=getPlacement&app=' + bid.params.siteId + '&placement=' + bid.params.placementId; - const data = this._getPayload(bid, bidderRequest); + const data = getPayload(bid, bidderRequest); return { method: 'POST', headers: {'Content-Type': 'application/json;charset=utf-8'}, @@ -42,117 +42,112 @@ export const spec = { height: adData.h, netRevenue: true, ttl: BID_TTL, - creativeId: adData.adId || 0, - currency: DEFAULT_CURRENCY, + creativeId: adData.adId || 1, + currency: adData.cur || DEFAULT_CURRENCY, referrer: data.data.ref, - mediaType: ads[0].ad.subtype, + mediaType: ads[0].ad.subtype === 'videoVast' ? VIDEO : BANNER, ad: adData.markup, - placement: data.placement, + adUnitCode: data.adUnitCode, + renderURL: data.renderURL, adData: adData }; - if (bidResponse.vastUrl === 'videoVast') { - bidResponse.vastUrl = adData.videos[0].url + + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastUrl = adData.videos[0] && adData.videos[0].url + } + + if (bidResponse.renderURL) { + bidResponse.renderer = newRenderer(bidResponse); } bidResponses.push(bidResponse); } return bidResponses; - }, - _getPayload: function (bid, bidderRequest) { - const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { - let r = Math.random() * 16 | 0; - let v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - const { params } = bid; - const { siteId, placementId } = params; - const { refererInfo, uspConsent, gdprConsent } = bidderRequest; - const mediation = {consent: '-1', gdpr: '-1'}; - if (gdprConsent) { - if (gdprConsent.consentString !== undefined) { - mediation.consent = gdprConsent.consentString; - } - if (gdprConsent.gdprApplies !== undefined) { - mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; - } + } +}; + +function getPayload (bid, bidderRequest) { + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { + let r = Math.random() * 16 | 0; + let v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + const { params, adUnitCode, bidId } = bid; + const { siteId, placementId, renderURL, pageCategory, keywords } = params; + const { refererInfo, uspConsent, gdprConsent } = bidderRequest; + const mediation = {consent: '-1', gdpr: '-1'}; + if (gdprConsent && 'gdprApplies' in gdprConsent) { + if (gdprConsent.consentString !== undefined) { + mediation.consent = gdprConsent.consentString; } - const payload = { - userSession, + if (gdprConsent.gdprApplies !== undefined) { + mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; + } + } + return { + userSession, + data: { + id: bidId, + action: 'getPlacement', + app: siteId, + placement: placementId, + adUnitCode, + renderURL, data: { - id: bid.bidId, - action: 'getPlacement', - app: siteId, - placement: placementId, - data: { - pagecat: params.pageCategory ? params.pageCategory.split(',').map(k => k.trim()) : [], - keywords: params.keywords ? params.keywords.split(',').map(k => k.trim()) : [], - lang_content: document.documentElement.lang, - lang: window.navigator.language, - // TODO: are these the correct refererInfo values? - domain: refererInfo.domain, - page: refererInfo.page, - ref: refererInfo.ref, - userids: _getUserIDs(), - geo: '', - }, - complianceData: { - child: '-1', - us_privacy: uspConsent, - dnt: window.navigator.doNotTrack, - iabConsent: {}, - mediation: { - consent: mediation.consent, - gdpr: mediation.gdpr, - } - }, - integration: 'JS', - omidpn: 'Displayio', - mediationPlatform: 0, - prebidVersion: BIDDER_VERSION, - device: { - w: window.screen.width, - h: window.screen.height, - connection_type: connection ? connection.effectiveType : '', + pagecat: pageCategory ? pageCategory.split(',').map(k => k.trim()) : [], + keywords: keywords ? keywords.split(',').map(k => k.trim()) : [], + lang_content: document.documentElement.lang, + lang: window.navigator.language, + domain: refererInfo.domain, + page: refererInfo.page, + ref: refererInfo.referer, + userids: bid.userIdAsEids || {}, + geo: '', + }, + complianceData: { + child: '-1', + us_privacy: uspConsent, + dnt: window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false, + iabConsent: {}, + mediation: { + consent: mediation.consent, + gdpr: mediation.gdpr, } + }, + integration: 'JS', + omidpn: 'Displayio', + mediationPlatform: 0, + prebidVersion: ADAPTER_VERSION, + device: { + w: window.screen.width, + h: window.screen.height, + connection_type: connection ? connection.effectiveType : '', } } - if (navigator.permissions) { - navigator.permissions.query({ name: 'geolocation' }) - .then((result) => { - if (result.state === 'granted') { - payload.data.data.geo = _getGeoData(); - } - }); - } - return payload } -}; +} + +function newRenderer(bid) { + const renderer = Renderer.install({ + id: bid.requestId, + url: bid.renderURL, + adUnitCode: bid.adUnitCode + }); -function _getUserIDs () { - let ids = {}; try { - ids = window.owpbjs.getUserIdsAsEids(); - } catch (e) {} - return ids; + renderer.setRender(webisRender); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; } -async function _getGeoData () { - let geoData = null; - const getCurrentPosition = () => { - return new Promise((resolve, reject) => - navigator.geolocation.getCurrentPosition(resolve, reject) - ); - } - try { - const position = await getCurrentPosition(); - let {latitude, longitude, accuracy} = position.coords; - geoData = { - 'lat': latitude, - 'lng': longitude, - 'precision': accuracy - }; - } catch (e) {} - return geoData +function webisRender(bid, doc) { + bid.renderer.push(() => { + const win = getWindowFromDocument(doc) || window; + win.webis.init(bid.adData, bid.adUnitCode, bid.params); + }) } registerBidder(spec); diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 96ec1fed754..95ce5c94e46 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -7,19 +7,19 @@ import { deepAccess, - logInfo, deepSetValue, - logError, isEmpty, isEmptyStr, + logError, + logInfo, logWarn, safeJSONParse } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { submodule } from '../src/hook.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { uspDataHandler } from '../src/adapterManager.js'; +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {uspDataHandler} from '../src/adapterManager.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; @@ -28,10 +28,11 @@ 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' // 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' ]; +const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); @@ -102,92 +103,27 @@ export const id5IdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function getId - * @param {SubmoduleConfig} config + * @param {SubmoduleConfig} submoduleConfig * @param {ConsentData} consentData * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(config, consentData, cacheIdObj) { - if (!hasRequiredConfig(config)) { + getId(submoduleConfig, consentData, cacheIdObj) { + if (!hasRequiredConfig(submoduleConfig)) { return undefined; } - const url = `https://id5-sync.com/g/v2/${config.params.partner}.json`; - const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const usp = uspDataHandler.getConsentData(); - const referer = getRefererInfo(); - const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : getLegacyCookieSignature(); - const data = { - 'partner': config.params.partner, - 'gdpr': hasGdpr, - 'nbPage': incrementNb(config.params.partner), - 'o': 'pbjs', - 'rf': referer.topmostLocation, - 'top': referer.reachedTop ? 1 : 0, - 'u': referer.stack[0] || window.location.href, - 'v': '$prebid.version$' - }; - - // pass in optional data, but only if populated - if (hasGdpr && typeof consentData.consentString !== 'undefined' && !isEmpty(consentData.consentString) && !isEmptyStr(consentData.consentString)) { - data.gdpr_consent = consentData.consentString; - } - if (typeof usp !== 'undefined' && !isEmpty(usp) && !isEmptyStr(usp)) { - data.us_privacy = usp; - } - if (typeof signature !== 'undefined' && !isEmptyStr(signature)) { - data.s = signature; - } - if (typeof config.params.pd !== 'undefined' && !isEmptyStr(config.params.pd)) { - data.pd = config.params.pd; - } - if (typeof config.params.provider !== 'undefined' && !isEmptyStr(config.params.provider)) { - data.provider = config.params.provider; - } - - const abTestingConfig = getAbTestingConfig(config); - if (abTestingConfig.enabled === true) { - data.ab_testing = { - enabled: true, - control_group_pct: abTestingConfig.controlGroupPct // The server validates - }; - } - - const resp = function (callback) { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'response received from the server', responseObj); - - resetNb(config.params.partner); - - if (responseObj.privacy) { - storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(responseObj.privacy), NB_EXP_DAYS); - } - - // TODO: remove after requiring publishers to use localstorage and - // all publishers have upgraded - if (config.storage.type === LOCAL_STORAGE) { - removeLegacyCookies(config.params.partner); - } - } catch (error) { - logError(LOG_PREFIX + error); - } - } - callback(responseObj); - }, - error: error => { + const resp = function (cbFunction) { + new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute() + .then(response => { + cbFunction(response) + }) + .catch(error => { logError(LOG_PREFIX + 'getId fetch encountered an error', error); - callback(); - } - }; - logInfo(LOG_PREFIX + 'requesting an ID from the server', data); - ajax(url, callbacks, JSON.stringify(data), { method: 'POST', withCredentials: true }); + cbFunction(); + }); }; - return { callback: resp }; + return {callback: resp}; }, /** @@ -212,6 +148,139 @@ export const id5IdSubmodule = { } }; +class IdFetchFlow { + constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData) { + this.submoduleConfig = submoduleConfig + this.gdprConsentData = gdprConsentData + this.cacheIdObj = cacheIdObj + this.usPrivacyData = usPrivacyData + } + + execute() { + return this.#callForConfig(this.submoduleConfig) + .then(fetchFlowConfig => { + return this.#callForExtensions(fetchFlowConfig.extensionsCall) + .then(extensionsData => { + return this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData) + }) + }) + .then(fetchCallResponse => { + try { + resetNb(this.submoduleConfig.params.partner); + if (fetchCallResponse.privacy) { + storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); + } + } catch (error) { + logError(LOG_PREFIX + error); + } + return fetchCallResponse; + }) + } + + #ajaxPromise(url, data, options) { + return new Promise((resolve, reject) => { + ajax(url, + { + success: function (res) { + resolve(res) + }, + error: function (err) { + reject(err) + } + }, data, options) + }) + } + + // eslint-disable-next-line no-dupe-class-members + #callForConfig(submoduleConfig) { + let url = submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only + return this.#ajaxPromise(url, JSON.stringify(submoduleConfig), {method: 'POST'}) + .then(response => { + let responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'config response received from the server', responseObj); + return responseObj; + }); + } + + // eslint-disable-next-line no-dupe-class-members + #callForExtensions(extensionsCallConfig) { + if (extensionsCallConfig === undefined) { + return Promise.resolve(undefined) + } + let extensionsUrl = extensionsCallConfig.url + let method = extensionsCallConfig.method || 'GET' + let data = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}) + return this.#ajaxPromise(extensionsUrl, data, {'method': method}) + .then(response => { + let responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'extensions response received from the server', responseObj); + return responseObj; + }) + } + + // eslint-disable-next-line no-dupe-class-members + #callId5Fetch(fetchCallConfig, extensionsData) { + let url = fetchCallConfig.url; + let additionalData = fetchCallConfig.overrides || {}; + let data = { + ...this.#createFetchRequestData(), + ...additionalData, + extensions: extensionsData + }; + return this.#ajaxPromise(url, JSON.stringify(data), {method: 'POST', withCredentials: true}) + .then(response => { + let responseObj = JSON.parse(response); + logInfo(LOG_PREFIX + 'fetch response received from the server', responseObj); + return responseObj; + }); + } + + // eslint-disable-next-line no-dupe-class-members + #createFetchRequestData() { + 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 = incrementNb(params.partner); + const data = { + 'partner': params.partner, + 'gdpr': hasGdpr, + 'nbPage': nbPage, + 'o': 'pbjs', + 'rf': referer.topmostLocation, + 'top': referer.reachedTop ? 1 : 0, + 'u': referer.stack[0] || window.location.href, + 'v': '$prebid.version$', + 'storage': this.submoduleConfig.storage + }; + + // pass in optional data, but only if populated + if (hasGdpr && this.gdprConsentData.consentString !== undefined && !isEmpty(this.gdprConsentData.consentString) && !isEmptyStr(this.gdprConsentData.consentString)) { + data.gdpr_consent = this.gdprConsentData.consentString; + } + if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) { + data.us_privacy = this.usPrivacyData; + } + if (signature !== undefined && !isEmptyStr(signature)) { + data.s = signature; + } + if (params.pd !== undefined && !isEmptyStr(params.pd)) { + data.pd = params.pd; + } + if (params.provider !== undefined && !isEmptyStr(params.provider)) { + data.provider = params.provider; + } + const abTestingConfig = params.abTesting || {enabled: false}; + + if (abTestingConfig.enabled) { + data.ab_testing = { + enabled: true, control_group_pct: abTestingConfig.controlGroupPct // The server validates + }; + } + return data + } +} + function hasRequiredConfig(config) { if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { logError(LOG_PREFIX + 'partner required to be defined as a number'); @@ -242,25 +311,29 @@ export function expDaysStr(expDays) { 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 resetNb(partnerId) { storeNbInCache(partnerId, 0); } function getLegacyCookieSignature() { let legacyStoredValue; - LEGACY_COOKIE_NAMES.forEach(function(cookie) { + LEGACY_COOKIE_NAMES.forEach(function (cookie) { if (storage.getCookie(cookie)) { legacyStoredValue = safeJSONParse(storage.getCookie(cookie)) || legacyStoredValue; } @@ -268,21 +341,6 @@ function getLegacyCookieSignature() { return (legacyStoredValue && legacyStoredValue.signature) || ''; } -/** - * Remove our legacy cookie values. Needed until we move all publishers - * to html5 storage in a future release - * @param {integer} partnerId - */ -function removeLegacyCookies(partnerId) { - logInfo(LOG_PREFIX + 'removing legacy cookies'); - LEGACY_COOKIE_NAMES.forEach(function(cookie) { - storage.setCookie(`${cookie}`, ' ', expDaysStr(-1)); - storage.setCookie(`${cookie}_nb`, ' ', expDaysStr(-1)); - storage.setCookie(`${cookie}_${partnerId}_nb`, ' ', expDaysStr(-1)); - storage.setCookie(`${cookie}_last`, ' ', expDaysStr(-1)); - }); -} - /** * This will make sure we check for expiration before accessing local storage * @param {string} key @@ -303,6 +361,7 @@ export function getFromLocalStorage(key) { storage.removeDataFromLocalStorage(key); return null; } + /** * Ensure that we always set an expiration in local storage since * by default it's not required @@ -315,14 +374,4 @@ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}`, value); } -/** - * gets the existing abTesting config or generates a default config with abTesting off - * - * @param {SubmoduleConfig|undefined} config - * @returns {Object} an object which always contains at least the property "enabled" - */ -function getAbTestingConfig(config) { - return deepAccess(config, 'params.abTesting', { enabled: false }); -} - submodule('userId', id5IdSubmodule); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index 11f8ffc5609..cf90290b1d8 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -29,7 +29,8 @@ pbjs.setConfig({ abTesting: { // optional enabled: true, // false by default controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) - } + }, + disableExtensions: false // optional }, storage: { type: 'html5', // "html5" is the required storage type @@ -43,21 +44,22 @@ pbjs.setConfig({ }); ``` -| Param under userSync.userIds[] | Scope | Type | Description | Example | +| Param under userSync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | -| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | -| params | Required | Object | Details for the ID5 ID. | | -| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | -| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | -| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | -| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | +| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | +| params | Required | Object | Details for the ID5 ID. | | +| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | +| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | +| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | | params.abTesting.controlGroupPct | Optional | Number | Must be a number between `0.0` and `1.0` (inclusive) and is used to determine the percentage of requests that fall into the control group (and thus not exposing the ID5 ID). For example, a value of `0.20` will result in 20% of requests without an ID5 ID and 80% with an ID. | `0.1` | -| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | -| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | -| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | -| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | -| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | +| params.disableExtensions | Optional | Boolean | Set this to `true` to force turn off extensions call. Default `false` | `true` or `false` | +| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | +| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | **ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid@id5.io](mailto:prebid@id5.io). diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 4996f0efaf0..c8fc8eb7a2a 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -19,6 +19,7 @@ import {Renderer} from '../src/Renderer.js'; import {createEidsArray} from './userId/eids.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {loadExternalScript} from '../src/adloader.js'; const BIDDER_CODE = 'improvedigital'; const CREATIVE_TTL = 300; @@ -212,7 +213,7 @@ export const spec = { ID_RESPONSE.buildAd(bid, bidRequest, bidObject); - ID_RAZR.addBidData({ + ID_RAZR.forwardBid({ bidRequest, bid }); @@ -640,37 +641,58 @@ const ID_OUTSTREAM = { }; const ID_RAZR = { - RENDERER_URL: 'https://razr.improvedigital.com/renderer.js', - addBidData({bid, bidRequest}) { - if (this.isValidBid(bid)) { - bid.renderer = Renderer.install({ - url: this.RENDERER_URL, - config: {bidRequest} - }); - bid.renderer.setRender(this.render); + RENDERER_URL: 'https://cdn.360yield.com/razr/tag.js', + + forwardBid({bidRequest, bid}) { + if (bid.mediaType !== BANNER) { + return; } - }, - isValidBid(bid) { - return bid && /razr:\/\//.test(bid.ad); + const cfg = { + prebid: { + bidRequest, + bid + } + }; + + const cfgStr = JSON.stringify(cfg).replace(/<\/script>/g, '\\x3C/script>'); + const s = ``; + bid.ad = bid.ad.replace(/]*>/, match => match + s); + + this.installListener(); }, - render(bid) { - const {bidRequest} = bid.renderer.getConfig(); - - const payload = { - type: 'prebid', - bidRequest, - bid, - config: mergeDeep( - {}, - config.getConfig('improvedigital.rendererConfig'), - deepAccess(bidRequest, 'params.rendererConfig') - ) - }; + installListener() { + if (this._listenerInstalled) { + return; + } + + window.addEventListener('message', function(e) { + const data = e.data?.razr?.load; + if (!data) { + return; + } + + if (e.source) { + data.source = e.source; + if (data.id) { + e.source.postMessage({ + razr: { + id: data.id + } + }, '*'); + } + } + + const ns = window.razr = window.razr || {}; + ns.q = ns.q || []; + ns.q.push(data); + + if (!ns.loaded) { + loadExternalScript(ID_RAZR.RENDERER_URL, BIDDER_CODE); + } + }); - const razr = window.razr = window.razr || {}; - razr.queue = razr.queue || []; - razr.queue.push(payload); + this._listenerInstalled = true; } }; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index ce4fdcc2431..61e518d306d 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -25,7 +25,6 @@ import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {Renderer} from '../src/Renderer.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -100,93 +99,6 @@ const VIDEO_PARAMS_ALLOW_LIST = [ 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext', 'playerSize', 'w', 'h' ]; -const NATIVE_ASSET_TYPES = { - TITLE: 100, - IMG: 200, - VIDEO: 300, - DATA: 400 -}; -const NATIVE_IMAGE_TYPES = { - ICON: 1, - MAIN: 3 -}; -const NATIVE_DATA_TYPES = { - SPONSORED: 1, - DESC: 2, - RATING: 3, - LIKES: 4, - DOWNLOADS: 5, - PRICE: 6, - SALEPRICE: 7, - PHONE: 8, - ADDRESS: 9, - DESC2: 10, - DISPLAYURL: 11, - CTATEXT: 12 -}; -const NATIVE_DATA_MAP = { - [NATIVE_DATA_TYPES.SPONSORED]: 'sponsoredBy', - [NATIVE_DATA_TYPES.DESC]: 'body', - [NATIVE_DATA_TYPES.RATING]: 'rating', - [NATIVE_DATA_TYPES.LIKES]: 'likes', - [NATIVE_DATA_TYPES.DOWNLOADS]: 'downloads', - [NATIVE_DATA_TYPES.PRICE]: 'price', - [NATIVE_DATA_TYPES.SALEPRICE]: 'salePrice', - [NATIVE_DATA_TYPES.PHONE]: 'phone', - [NATIVE_DATA_TYPES.ADDRESS]: 'address', - [NATIVE_DATA_TYPES.DESC2]: 'body2', - [NATIVE_DATA_TYPES.DISPLAYURL]: 'displayUrl', - [NATIVE_DATA_TYPES.CTATEXT]: 'cta' -}; -const NATIVE_ASSETS_MAP = { - 'title': { assetType: NATIVE_ASSET_TYPES.TITLE }, - 'icon': { assetType: NATIVE_ASSET_TYPES.IMG, subtype: NATIVE_IMAGE_TYPES.ICON }, - 'image': { assetType: NATIVE_ASSET_TYPES.IMG, subtype: NATIVE_IMAGE_TYPES.MAIN }, - 'sponsoredBy': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.SPONSORED }, - 'body': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DESC }, - 'rating': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.RATING }, - 'likes': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.LIKES }, - 'downloads': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DOWNLOADS }, - 'price': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.PRICE }, - 'salePrice': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.SALEPRICE }, - 'phone': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.PHONE }, - 'address': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.ADDRESS }, - 'body2': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DESC2 }, - 'displayUrl': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DISPLAYURL }, - 'cta': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.CTATEXT }, - 'video': { assetType: NATIVE_ASSET_TYPES.VIDEO } -}; -const NATIVE_ALLOWED_PROPERTIES = [ - 'rendererUrl', - 'sendTargetingKeys', - 'adTemplate', - 'type', - 'ext', - 'privacyLink', - 'clickUrl', - 'privacyIcon' -]; -const NATIVE_ASSET_DEFAULT = { - TITLE: { - LEN: 25 - }, - VIDEO: { - MIMES: [ - 'video/mp4', - 'video/webm' - ], - MINDURATION: 0, - MAXDURATION: 120, - PROTOCOLS: [2, 3, 5, 6], - } -}; -const NATIVE_EVENT_TYPES = { - IMRESSION: 1 -}; -const NATIVE_EVENT_TRACKING_METHOD = { - IMG: 1, - JS: 2 -}; const LOCAL_STORAGE_KEY = 'ixdiag'; let hasRegisteredHandler = false; export const storage = getStorageManager({gvlid: GLOBAL_VENDOR_ID, bidderCode: BIDDER_CODE}); @@ -307,50 +219,13 @@ function bidToVideoImp(bid) { */ function bidToNativeImp(bid) { const imp = bidToImp(bid); - const nativeAdUnitRef = deepAccess(bid, 'mediaTypes.native'); - - const assets = []; - - // Convert all native assets to imp object - for (const [adUnitProperty, adUnitValues] of Object.entries(nativeAdUnitRef)) { - if (!NATIVE_ASSETS_MAP[adUnitProperty]) { - continue; - } - - const { assetType, subtype } = NATIVE_ASSETS_MAP[adUnitProperty]; - let asset; - switch (assetType) { - case NATIVE_ASSET_TYPES.TITLE: - asset = createNativeTitleRequest(adUnitValues); - break; - case NATIVE_ASSET_TYPES.IMG: - asset = createNativeImgRequest(adUnitValues, subtype); - break; - case NATIVE_ASSET_TYPES.VIDEO: - asset = createNativeVideoRequest(adUnitValues); - break; - case NATIVE_ASSET_TYPES.DATA: - asset = createNativeDataRequest(adUnitValues, subtype); - break; - } - asset.id = assetType + (subtype || 0); - assets.push(asset); - } - - if (assets.length === 0) { - logWarn('IX Bid Adapter: Native bid does not contain recognised assets in [mediaTypes.native]'); - return {}; - } - const request = { - assets: assets, - ver: '1.2', - eventtrackers: [{ - event: 1, - methods: [1, 2] - }], - privacy: 1 - }; + const request = bid.nativeOrtbRequest + request.eventtrackers = [{ + event: 1, + methods: [1, 2] + }]; + request.privacy = 1; imp.native = { request: JSON.stringify(request), @@ -365,80 +240,6 @@ function bidToNativeImp(bid) { return imp; } -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @returns {object} IX impression asset object - */ -function createNativeTitleRequest(bidAsset) { - return { - required: bidAsset.required ? 1 : 0, - title: { - len: bidAsset.len ? bidAsset.len : NATIVE_ASSET_DEFAULT.TITLE.LEN, - ext: bidAsset.ext - } - } -} - -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @param {int} type The image type - * @returns {object} IX impression asset object - */ -function createNativeImgRequest(bidAsset, type) { - let asset = { - required: bidAsset.required ? 1 : 0, - img: { - type: type, - mimes: bidAsset.mimes, - ext: bidAsset.ext - } - } - - if (bidAsset.hasOwnProperty('sizes') && bidAsset.sizes.length === 2) { - asset.img.wmin = bidAsset.sizes[0]; - asset.img.hmin = bidAsset.sizes[1]; - } - - return asset -} - -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @returns {object} IX impression asset object - */ -function createNativeVideoRequest(bidAsset) { - return { - required: bidAsset.required ? 1 : 0, - video: { - mimes: bidAsset.mimes ? bidAsset.mimes : NATIVE_ASSET_DEFAULT.VIDEO.MIMES, - minduration: bidAsset.minduration ? bidAsset.minduration : NATIVE_ASSET_DEFAULT.VIDEO.MINDURATION, - maxduration: bidAsset.maxduration ? bidAsset.maxduration : NATIVE_ASSET_DEFAULT.VIDEO.MAXDURATION, - protocols: bidAsset.protocols ? bidAsset.protocols : NATIVE_ASSET_DEFAULT.VIDEO.PROTOCOLS, - ext: bidAsset.ext - } - } -} - -/** - * Converts native bid asset to a native impression asset - * @param {object} bidAsset PBJS bid asset object - * @param {int} type The image type - * @returns {object} IX impression asset object - */ -function createNativeDataRequest(bidAsset, type) { - return { - required: bidAsset.required ? 1 : 0, - data: { - type: type, - len: bidAsset.len, - ext: bidAsset.ext - } - } -} - /** * Converts an incoming PBJS bid to an IX Impression * @param {object} bid PBJS bid object @@ -559,7 +360,7 @@ function parseBid(rawBid, currency, bidRequest) { bid.mediaTypes = bidRequest.mediaTypes; bid.ttl = isValidExpiry ? rawBid.exp : VIDEO_TIME_TO_LIVE; } else if (parsedAdm && parsedAdm.native) { - bid.native = interpretNativeAdm(parsedAdm.native); + bid.native = {ortb: parsedAdm.native}; bid.width = rawBid.w ? rawBid.w : 1; bid.height = rawBid.h ? rawBid.h : 1; bid.mediaType = NATIVE; @@ -583,84 +384,6 @@ function parseBid(rawBid, currency, bidRequest) { return bid; } -/** - * Parse native adm and set native asset key names recognized by Prebid.js - * @param {string} adm Native adm complience - */ -function interpretNativeAdm(nativeResponse) { - const native = { - clickUrl: nativeResponse.link.url, - privacyLink: nativeResponse.privacy - }; - - for (const asset of nativeResponse.assets) { - const subtype = asset.id % 100; - const assetType = asset.id - subtype; - - switch (assetType) { - case NATIVE_ASSET_TYPES.TITLE: - native.title = asset.title && asset.title.text; - break; - case NATIVE_ASSET_TYPES.IMG: - const image = { - url: asset.img && asset.img.url, - height: asset.img && asset.img.h, - width: asset.img && asset.img.w - }; - native[subtype === NATIVE_IMAGE_TYPES.ICON ? 'icon' : 'image'] = image; - break; - case NATIVE_ASSET_TYPES.VIDEO: - native.video = asset.video && asset.video.vasttag; - break; - case NATIVE_ASSET_TYPES.DATA: - setDataAsset(native, asset, subtype); - break; - default: - logWarn(`IX Bid Adapter: native asset ID ${asset.id} could not be recognized`); - } - } - - setTrackers(native, nativeResponse); - return native; -} - -function setDataAsset(native, asset, type) { - if (!(type in NATIVE_DATA_MAP)) { - logWarn(`IX Bid Adapter: native data asset type ${type} is not supported`); - return; - } - native[NATIVE_DATA_MAP[type]] = asset.data && asset.data.value; -} - -function setTrackers(native, nativeResponse) { - native.impressionTrackers = [] - - if (Array.isArray(nativeResponse.imptrackers)) { - native.impressionTrackers.push(...nativeResponse.imptrackers) - } - - if (Array.isArray(nativeResponse.link.clicktrackers)) { - native.impressionTrackers.push(...nativeResponse.link.clicktrackers) - } - - if (Array.isArray(nativeResponse.eventtrackers)) { - nativeResponse.eventtrackers.forEach(tracker => { - if (tracker.event !== NATIVE_EVENT_TYPES.IMRESSION) { - return - } - - switch (tracker.method) { - case NATIVE_EVENT_TRACKING_METHOD.IMG: - native.impressionTrackers.push(tracker.url); - break; - case NATIVE_EVENT_TRACKING_METHOD.JS: - native.javascriptTrackers = ``; - break; - } - }) - } -} - /** * Determines whether or not the given object is valid size format. * @@ -758,25 +481,13 @@ function isValidBidFloorParams(bidFloor, bidFloorCur) { bidFloorCur.match(curRegex)); } -function nativeMediaTypeValid(nativeObj) { - if (nativeObj === undefined) { - return true; - } - - let hasValidAsset = false; - - for (const property in nativeObj) { - if (!(property in NATIVE_ASSETS_MAP) && !NATIVE_ALLOWED_PROPERTIES.includes(property)) { - logError('IX Bid Adapter: native', { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); - return false; - } - - if (property in NATIVE_ASSETS_MAP) { - hasValidAsset = true; - } +function nativeMediaTypeValid(bid) { + const nativeMediaTypes = deepAccess(bid, 'mediaTypes.native'); + if (nativeMediaTypes === undefined) { + return true } - return hasValidAsset; + return bid.nativeOrtbRequest && Array.isArray(bid.nativeOrtbRequest.assets) && bid.nativeOrtbRequest.assets.length > 0 } /** @@ -837,8 +548,6 @@ function getEidInfo(allEids) { * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); // Always use secure HTTPS protocol. let baseUrl = SECURE_BID_URL; // Get ids from Prebid User ID Modules @@ -966,7 +675,6 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // Use the siteId in the first bid request as the main siteId. siteID = validBidRequests[0].params.siteId; payload.s = siteID; - payload.v = version; if (version) { payload.v = version; } @@ -1603,7 +1311,6 @@ export const spec = { const paramsSize = deepAccess(bid, 'params.size'); const mediaTypeBannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); const mediaTypeVideoRef = deepAccess(bid, 'mediaTypes.video'); - const mediaTypeNativeRef = deepAccess(bid, 'mediaTypes.native'); const mediaTypeVideoPlayerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); const hasBidFloor = bid.params.hasOwnProperty('bidFloor'); const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); @@ -1670,7 +1377,7 @@ export const spec = { } } - return nativeMediaTypeValid(mediaTypeNativeRef); + return nativeMediaTypeValid(bid); }, /** diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index f873e5b5c29..87d2e53568f 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -26,11 +26,12 @@ export const spec = { _each(validBidRequests, function(bid) { window.nmmRefreshCounts[bid.adUnitCode] = window.nmmRefreshCounts[bid.adUnitCode] || 0; const id = getPlacementId(bid) + let sizes = bid.sizes + if (sizes && !Array.isArray(sizes[0])) sizes = [sizes] + + const site = getSiteObj() + const device = getDeviceObj() - if (bid.sizes && !Array.isArray(bid.sizes[0])) bid.sizes = [bid.sizes] - if (!bid.ortb2) bid.ortb2 = {} - if (!bid.ortb2.device) bid.ortb2.device = {} - bid.ortb2.device.referrer = (getRefererInfo && getRefererInfo().ref) || '' const postBody = { 'id': bid.auctionId, 'ext': { @@ -46,10 +47,11 @@ export const spec = { 'scrollTop': window.pageYOffset || document.documentElement.scrollTop } }, - ...bid.ortb2, + device, + site, 'imp': [{ 'banner': { - 'format': (bid.sizes || []).map(s => { return {w: s[0], h: s[1]} }) + 'format': (sizes || []).map(s => { return {w: s[0], h: s[1]} }) }, 'ext': { 'prebid': { @@ -198,4 +200,21 @@ function getTopWindow(curWindow, nesting = 0) { } } +function getSiteObj() { + const refInfo = (getRefererInfo && getRefererInfo()) || {} + + return { + page: refInfo.page, + ref: refInfo.ref, + domain: refInfo.domain + } +} + +function getDeviceObj() { + return { + w: window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth || 0, + h: window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight || 0, + } +} + registerBidder(spec); diff --git a/modules/rasBidAdapter.js b/modules/rasBidAdapter.js index 909b6a7b795..7bc3cf66b0d 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,3 +1,4 @@ +import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { isEmpty, getAdUnitSizes, parseSizesInput, deepAccess } from '../src/utils.js'; @@ -92,11 +93,18 @@ const getSlots = (bidRequests) => { const batchSize = bidRequests.length; for (let i = 0; i < batchSize; i++) { const adunit = bidRequests[i]; + const slotSequence = utils.deepAccess(adunit, 'params.slotSequence'); + const sizes = parseSizesInput(getAdUnitSizes(adunit)).join(','); + queryString += `&slot${i}=${encodeURIComponent(adunit.params.slot)}&id${i}=${encodeURIComponent(adunit.bidId)}&composition${i}=CHILD`; + if (sizes.length) { queryString += `&iusizes${i}=${encodeURIComponent(sizes)}`; } + if (slotSequence !== undefined) { + queryString += `&pos${i}=${encodeURIComponent(slotSequence)}`; + } } return queryString; }; diff --git a/modules/rasBidAdapter.md b/modules/rasBidAdapter.md index 5cf75c3446d..384ba0b611f 100644 --- a/modules/rasBidAdapter.md +++ b/modules/rasBidAdapter.md @@ -34,17 +34,18 @@ var adUnits = [{ # Parameters -| Name | Scope | Type | Description | Example -| --- | --- | --- | --- | --- -| network | required | String | Specific identifier provided by RAS | `"4178463"` -| site | required | String | Specific identifier name (case-insensitive) that is associated with this ad unit and provided by RAS | `"example_com"` -| area | required | String | Ad unit category name; only case-insensitive alphanumeric with underscores and hyphens are allowed | `"sport"` -| slot | required | String | Ad unit placement name (case-insensitive) provided by RAS | `"slot"` -| pageContext | optional | Object | Web page context data | `{}` -| pageContext.dr | optional | String | Document referrer URL address | `"https://example.com/"` -| pageContext.du | optional | String | Document URL address | `"https://example.com/sport/football/article.html?id=932016a5-02fc-4d5c-b643-fafc2f270f06"` +| Name | Scope | Type | Description | Example +| --- | --- | --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- +| network | required | String | Specific identifier provided by RAS | `"4178463"` +| site | required | String | Specific identifier name (case-insensitive) that is associated with this ad unit and provided by RAS | `"example_com"` +| area | required | String | Ad unit category name; only case-insensitive alphanumeric with underscores and hyphens are allowed | `"sport"` +| slot | required | String | Ad unit placement name (case-insensitive) provided by RAS | `"slot"` +| slotSequence | optional | Number | Ad unit sequence position provided by RAS | `1` +| pageContext | optional | Object | Web page context data | `{}` +| pageContext.dr | optional | String | Document referrer URL address | `"https://example.com/"` +| pageContext.du | optional | String | Document URL address | `"https://example.com/sport/football/article.html?id=932016a5-02fc-4d5c-b643-fafc2f270f06"` | pageContext.dv | optional | String | Document virtual address as slash-separated path that may consist of any number of parts (case-insensitive alphanumeric with underscores and hyphens); first part should be the same as `site` value and second as `area` value; next parts may reflect website navigation | `"example_com/sport/football"` -| pageContext.keyWords | optional | String[] | List of keywords associated with this ad unit; only case-insensitive alphanumeric with underscores and hyphens are allowed | `["euro", "lewandowski"]` -| pageContext.keyValues | optional | Object | Key-values associated with this ad unit (case-insensitive); following characters are not allowed in the values: `" ' = ! + # * ~ ; ^ ( ) < > [ ] & @` | `{}` -| pageContext.keyValues.ci | optional | String | Content unique identifier | `"932016a5-02fc-4d5c-b643-fafc2f270f06"` -| pageContext.keyValues.adunit | optional | String | Ad unit name | `"example_com/sport"` +| pageContext.keyWords | optional | String[] | List of keywords associated with this ad unit; only case-insensitive alphanumeric with underscores and hyphens are allowed | `["euro", "lewandowski"]` +| pageContext.keyValues | optional | Object | Key-values associated with this ad unit (case-insensitive); following characters are not allowed in the values: `" ' = ! + # * ~ ; ^ ( ) < > [ ] & @` | `{}` +| pageContext.keyValues.ci | optional | String | Content unique identifier | `"932016a5-02fc-4d5c-b643-fafc2f270f06"` +| pageContext.keyValues.adunit | optional | String | Ad unit name | `"example_com/sport"` diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index c6100ac8b40..3c841cc4d8a 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -11,6 +11,7 @@ const OUTSTREAM_REDNERER_URL = 'https://mtrx.go.sonobi.com/sbi_outstream_rendere export const spec = { code: BIDDER_CODE, + gvlid: 104, supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. diff --git a/package-lock.json b/package-lock.json index 749fafaebc4..a6ae1e5a3e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "7.10.0-pre", + "version": "7.12.0-pre", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 58898eb0110..a9a4c120c48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "7.11.0-pre", + "version": "7.12.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/adloader.js b/src/adloader.js index 1e7995a9dc6..6b7427d3e52 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -18,7 +18,8 @@ const _approvedLoadExternalJSList = [ 'ftrackId', 'inskin', 'hadron', - 'medianet' + 'medianet', + 'improvedigital' ] /** diff --git a/src/adserver.js b/src/adserver.js index 8d99b29c3ef..db7aaaa1dc8 100644 --- a/src/adserver.js +++ b/src/adserver.js @@ -1,61 +1,5 @@ -import { formatQS } from './utils.js'; -import { targeting } from './targeting.js'; import {hook} from './hook.js'; -// Adserver parent class -const AdServer = function(attr) { - this.name = attr.adserver; - this.code = attr.code; - this.getWinningBidByCode = function() { - return targeting.getWinningBids(this.code)[0]; - }; -}; - -// DFP ad server -// TODO: this seems to be unused? -export function dfpAdserver(options, urlComponents) { - var adserver = new AdServer(options); - adserver.urlComponents = urlComponents; - - var dfpReqParams = { - 'env': 'vp', - 'gdfp_req': '1', - 'impl': 's', - 'unviewed_position_start': '1' - }; - - var dfpParamsWithVariableValue = ['output', 'iu', 'sz', 'url', 'correlator', 'description_url', 'hl']; - - var getCustomParams = function(targeting) { - return encodeURIComponent(formatQS(targeting)); - }; - - adserver.appendQueryParams = function() { - var bid = adserver.getWinningBidByCode(); - if (bid) { - this.urlComponents.search.description_url = encodeURIComponent(bid.vastUrl); - this.urlComponents.search.cust_params = getCustomParams(bid.adserverTargeting); - this.urlComponents.search.correlator = Date.now(); - } - }; - - adserver.verifyAdserverTag = function() { - for (var key in dfpReqParams) { - if (!this.urlComponents.search.hasOwnProperty(key) || this.urlComponents.search[key] !== dfpReqParams[key]) { - return false; - } - } - for (var i in dfpParamsWithVariableValue) { - if (!this.urlComponents.search.hasOwnProperty(dfpParamsWithVariableValue[i])) { - return false; - } - } - return true; - }; - - return adserver; -}; - /** * return the GAM PPID, if available (eid for the userID configured with `userSync.ppidSource`) */ diff --git a/src/native.js b/src/native.js index 470ac0b9a32..0e48470d300 100644 --- a/src/native.js +++ b/src/native.js @@ -735,6 +735,7 @@ function toLegacyResponse(ortbResponse, ortbRequest) { const legacyResponse = {}; const requestAssets = ortbRequest?.assets || []; legacyResponse.clickUrl = ortbResponse.link.url; + legacyResponse.privacyLink = ortbResponse.privacy; for (const asset of ortbResponse?.assets || []) { const requestAsset = requestAssets.find(reqAsset => asset.id === reqAsset.id); if (asset.title) { diff --git a/test/spec/modules/adriverBidAdapter_spec.js b/test/spec/modules/adriverBidAdapter_spec.js index 12c0a15fb06..33084877c14 100644 --- a/test/spec/modules/adriverBidAdapter_spec.js +++ b/test/spec/modules/adriverBidAdapter_spec.js @@ -297,25 +297,18 @@ describe('adriverAdapter', function () { { adrcid: undefined } ] cookieValues.forEach(cookieValue => describe('test cookie exist or not behavior', function () { - let expectedValues = { - adrcid: cookieValue.adrcid, - at: '', - cur: '', - tmax: '', - site: '', - id: '', - user: '', - device: '', - imp: '' - } + let expectedValues = [ + 'buyerid', + 'ext' + ] it('check adrcid if it exists', function () { bidRequests[0].userId.adrcid = cookieValue.adrcid; const payload = JSON.parse(spec.buildRequests(bidRequests).data); if (cookieValue.adrcid) { - expect(Object.keys(payload)).to.have.members(Object.keys(expectedValues)); + expect(Object.keys(payload.user)).to.have.members(expectedValues); } else { - expect(payload.adrcid).to.equal(undefined); + expect(payload.user.buyerid).to.equal(0); } }); })); diff --git a/test/spec/modules/adriverIdSystem_spec.js b/test/spec/modules/adriverIdSystem_spec.js index 29d965d5ed4..bc7d3f191d2 100644 --- a/test/spec/modules/adriverIdSystem_spec.js +++ b/test/spec/modules/adriverIdSystem_spec.js @@ -32,7 +32,6 @@ describe('AdriverIdSystem', function () { expect(request.url).to.include('https://ad.adriver.ru/cgi-bin/json.cgi'); request.respond(503, null, 'Unavailable'); expect(logErrorStub.calledOnce).to.be.true; - expect(callbackSpy.calledOnce).to.be.true; }); it('test call user sync url with the right params', function() { @@ -67,16 +66,12 @@ describe('AdriverIdSystem', function () { let request = server.requests[0]; request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ adrcid: response.adrcid })); - let expectedExpiration = new Date(); - expectedExpiration.setTime(expectedExpiration.getTime() + 86400 * 1825 * 1000); + let now = new Date(); + now.setTime(now.getTime() + 86400 * 1825 * 1000); const minimalDate = new Date(0).toString(); - function dateStringFor(date, maxDeltaMs = 2000) { - return sinon.match((val) => Math.abs(date.getTime() - new Date(val).getTime()) <= maxDeltaMs) - } - if (response.adrcid) { - expect(setCookieStub.calledWith('adrcid', response.adrcid, dateStringFor(expectedExpiration))).to.be.true; + expect(setCookieStub.calledWith('adrcid', response.adrcid, now.toUTCString())).to.be.true; expect(setLocalStorageStub.calledWith('adrcid', response.adrcid)).to.be.true; } else { expect(setCookieStub.calledWith('adrcid', '', minimalDate)).to.be.false; diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index a9b9724da3a..d0ef69ccf08 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -20,6 +20,7 @@ const aliasEP = { janet: 'https://ghb.bidder.jmgads.com/v2/auction/', pgam: 'https://ghb.pgamssp.com/v2/auction/', ocm: 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', + vidcrunchllc: 'https://ghb.platform.vidcrunch.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 7252232676f..7af5f5d77a2 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -13,7 +13,7 @@ import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; import { config } from '../../../src/config.js'; import * as storageManager from 'src/storageManager.js'; -import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; +import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { let utilsMock, sandbox; @@ -707,7 +707,7 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].sizes[0]).to.equal('undefinedxundefined'); }); - it('should properly detect and get sizes of native sizeless banner', function () { + it('should properly detect and forward native flag', function () { const bidRequests = [ { mediaTypes: { @@ -722,11 +722,10 @@ describe('The Criteo bidding adapter', function () { ]; const request = spec.buildRequests(bidRequests, bidderRequest); const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('2x2'); + expect(ortbRequest.slots[0].native).to.equal(true); }); - it('should properly detect and get size of native sizeless banner', function () { + it('should properly detect and forward native flag', function () { const bidRequests = [ { mediaTypes: { @@ -741,8 +740,7 @@ describe('The Criteo bidding adapter', function () { ]; const request = spec.buildRequests(bidRequests, bidderRequest); const ortbRequest = request.data; - expect(ortbRequest.slots[0].sizes).to.have.lengthOf(1); - expect(ortbRequest.slots[0].sizes[0]).to.equal('2x2'); + expect(ortbRequest.slots[0].native).to.equal(true); }); it('should properly build a networkId request', function () { @@ -1258,11 +1256,13 @@ describe('The Criteo bidding adapter', function () { if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', - floor: 1.0}; + floor: 1.0 + }; } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', - floor: 2.0}; + floor: 2.0 + }; } else { return {} } @@ -1273,9 +1273,10 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].ext.floors).to.deep.equal({ 'banner': { - '300x250': {'currency': 'USD', 'floor': 1}, - '728x90': {'currency': 'USD', 'floor': 2} - }}); + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }); }); it('should properly build a video request with several player sizes with floors', function () { @@ -1297,11 +1298,13 @@ describe('The Criteo bidding adapter', function () { if (inputParams.mediaType === VIDEO && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', - floor: 1.0}; + floor: 1.0 + }; } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', - floor: 2.0}; + floor: 2.0 + }; } else { return {} } @@ -1312,9 +1315,10 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].ext.floors).to.deep.equal({ 'video': { - '300x250': {'currency': 'USD', 'floor': 1}, - '728x90': {'currency': 'USD', 'floor': 2} - }}); + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }); }); it('should properly build a multi format request with floors', function () { @@ -1347,19 +1351,23 @@ describe('The Criteo bidding adapter', function () { if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', - floor: 1.0}; + floor: 1.0 + }; } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', - floor: 2.0}; + floor: 2.0 + }; } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 640 && inputParams.size[1] === 480) { return { currency: 'EUR', - floor: 3.2}; + floor: 3.2 + }; } else if (inputParams.mediaType === NATIVE && inputParams.size === '*') { return { currency: 'YEN', - floor: 4.99}; + floor: 4.99 + }; } else { return {} } @@ -1371,15 +1379,16 @@ describe('The Criteo bidding adapter', function () { expect(request.data.slots[0].ext.data.someContextAttribute).to.deep.equal('abc'); expect(request.data.slots[0].ext.floors).to.deep.equal({ 'banner': { - '300x250': {'currency': 'USD', 'floor': 1}, - '728x90': {'currency': 'USD', 'floor': 2} + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } }, 'video': { - '640x480': {'currency': 'EUR', 'floor': 3.2} + '640x480': { 'currency': 'EUR', 'floor': 3.2 } }, 'native': { - '*': {'currency': 'YEN', 'floor': 4.99} - }}); + '*': { 'currency': 'YEN', 'floor': 4.99 } + } + }); }); }); diff --git a/test/spec/modules/dgkeywordRtdProvider_spec.js b/test/spec/modules/dgkeywordRtdProvider_spec.js index a145f429557..5ecec48f6b4 100644 --- a/test/spec/modules/dgkeywordRtdProvider_spec.js +++ b/test/spec/modules/dgkeywordRtdProvider_spec.js @@ -293,8 +293,8 @@ describe('Digital Garage Keyword Module', function () { moduleConfig, null ); + const request = server.requests[0]; setTimeout(() => { - const request = server.requests[0]; if (request) { request.respond( 200, diff --git a/test/spec/modules/displayioBidAdapter_spec.js b/test/spec/modules/displayioBidAdapter_spec.js index dfeb67fb467..56b8b85384b 100644 --- a/test/spec/modules/displayioBidAdapter_spec.js +++ b/test/spec/modules/displayioBidAdapter_spec.js @@ -1,5 +1,5 @@ -import { expect } from 'chai' import {spec} from 'modules/displayioBidAdapter.js' +import {BANNER} from '/src/mediaTypes' describe('Displayio adapter', function () { const BIDDER = 'displayio' @@ -12,10 +12,7 @@ describe('Displayio adapter', function () { mediaTypes: { banner: { sizes: [[320, 480]] - }, - video: { - sizes: [[360, 640]] - }, + } }, params: { siteId: 1, @@ -128,53 +125,6 @@ describe('Displayio adapter', function () { }) }) - describe('_getPayload', function () { - const payload = spec._getPayload(bidRequests[0], bidderRequest) - it('should not be empty', function() { - expect(payload).to.not.be.empty - }) - - it('should have userSession', function() { - expect(payload.userSession).to.be.a('string') - }) - - it('should have data object', function() { - expect(payload.data).to.be.a('object') - }) - - it('should have complianceData object', function() { - expect(payload.data.complianceData).to.be.a('object') - }) - - it('should have device object', function() { - expect(payload.data.device).to.be.a('object') - }) - - it('should have omidpn', function() { - expect(payload.data.omidpn).to.be.a('string') - }) - - it('should have integration', function() { - expect(payload.data.integration).to.be.a('string') - }) - - it('should have bidId', function() { - expect(payload.data.id).to.not.be.empty - }) - - it('should have action getPlacement', function() { - expect(payload.data.action).to.be.equal('getPlacement') - }) - - it('should have app parameter', function() { - expect(payload.data.app).to.be.a('number') - }) - - it('should have placement parameter', function() { - expect(payload.data.placement).to.be.a('number') - }) - }) - describe('interpretResponse', function () { const response = { body: { @@ -199,6 +149,8 @@ describe('Displayio adapter', function () { data: { data: { id: 'id_001', + adUnitCode: 'test-div', + renderURL: 'testprebid.com/render.js', data: { ref: 'testprebid.com' } @@ -235,5 +187,17 @@ describe('Displayio adapter', function () { it('should have ad', function() { expect(ir.ad).to.be.a('string') }) + + it('should have mediaType', function() { + expect(ir.mediaType).to.be.equal(BANNER) + }) + + it('should have adUnitCode', function() { + expect(ir.adUnitCode).to.be.a('string') + }) + + it('should have renderURL', function() { + expect(ir.renderURL).to.be.a('string') + }) }) }) diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index a54542f7278..8c0f8ad9cf3 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -5,28 +5,36 @@ import { ID5_PRIVACY_STORAGE_NAME, ID5_STORAGE_NAME, id5IdSubmodule, - nbCacheName, storage, + nbCacheName, + storage, storeInLocalStorage, storeNbInCache, } from 'modules/id5IdSystem.js'; import {coreStorage, init, requestBidsHook, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import {server} from 'test/mocks/xhr.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; +import {uspDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; let expect = require('chai').expect; -describe('ID5 ID System', function() { +describe('ID5 ID System', function () { const ID5_MODULE_NAME = 'id5Id'; const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase(); const ID5_SOURCE = 'id5-sync.com'; const ID5_TEST_PARTNER_ID = 173; const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`; + const ID5_API_CONFIG_URL = `https://id5-sync.com/api/config/prebid`; + const ID5_EXTENSIONS_ENDPOINT = 'https://extensions.id5-sync.com/test'; + const ID5_API_CONFIG = { + fetchCall: { + url: ID5_ENDPOINT + } + }; const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID); const ID5_STORED_ID = 'storedid5id'; const ID5_STORED_SIGNATURE = '123456'; @@ -58,6 +66,7 @@ describe('ID5 ID System', function() { } } } + function getId5ValueConfig(value) { return { name: ID5_MODULE_NAME, @@ -68,6 +77,7 @@ describe('ID5 ID System', function() { } } } + function getUserSyncConfig(userIds) { return { userSync: { @@ -76,12 +86,15 @@ describe('ID5 ID System', function() { } } } + function getFetchLocalStorageConfig() { return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'html5')]); } + function getValueConfig(value) { return getUserSyncConfig([getId5ValueConfig(value)]); } + function getAdUnitMock(code = 'adUnit-code') { return { code, @@ -91,15 +104,81 @@ describe('ID5 ID System', function() { }; } + function callSubmoduleGetId(config, consentData, cacheIdObj) { + return new Promise((resolve) => { + id5IdSubmodule.getId(config, consentData, cacheIdObj).callback((response) => { + resolve(response) + }) + }); + } + + class XhrServerMock { + constructor(server) { + this.currentRequestIdx = 0 + this.server = server + } + + expectFirstRequest() { + return this.#expectRequest(0); + } + + expectNextRequest() { + return this.#expectRequest(++this.currentRequestIdx) + } + + expectConfigRequest() { + return this.expectFirstRequest() + .then(configRequest => { + expect(configRequest.url).is.eq(ID5_API_CONFIG_URL); + expect(configRequest.method).is.eq('POST'); + return configRequest; + }) + } + + respondWithConfigAndExpectNext(configRequest, config = ID5_API_CONFIG) { + configRequest.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(config)); + return this.expectNextRequest() + } + + expectFetchRequest() { + return this.expectConfigRequest() + .then(configRequest => { + return this.respondWithConfigAndExpectNext(configRequest, ID5_API_CONFIG); + }).then(request => { + expect(request.url).is.eq(ID5_API_CONFIG.fetchCall.url); + expect(request.method).is.eq('POST'); + return request; + }) + } + + #expectRequest(index) { + let server = this.server + return new Promise(function (resolve) { + (function waitForCondition() { + if (server.requests && server.requests.length > index) return resolve(server.requests[index]); + setTimeout(waitForCondition, 30); + })(); + }) + .then(request => { + return request + }); + } + + hasReceivedAnyRequest() { + let requests = this.server.requests; + return requests && requests.length > 0; + } + } + before(() => { hook.ready(); }); - describe('Check for valid publisher config', function() { - it('should fail with invalid config', function() { + describe('Check for valid publisher config', function () { + it('should fail with invalid config', function () { // no config - expect(id5IdSubmodule.getId()).to.be.eq(undefined); - expect(id5IdSubmodule.getId({ })).to.be.eq(undefined); + expect(id5IdSubmodule.getId()).is.eq(undefined); + expect(id5IdSubmodule.getId({})).is.eq(undefined); // valid params, invalid storage expect(id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined); @@ -113,7 +192,7 @@ describe('ID5 ID System', function() { expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined); }); - it('should warn with non-recommended storage params', function() { + it('should warn with non-recommended storage params', function () { let logWarnStub = sinon.stub(utils, 'logWarn'); id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } }); @@ -126,189 +205,457 @@ describe('ID5 ID System', function() { }); }); - describe('Xhr Requests from getId()', function() { - const responseHeader = { 'Content-Type': 'application/json' }; - let callbackSpy = sinon.spy(); + describe('Xhr Requests from getId()', function () { + const responseHeader = {'Content-Type': 'application/json'}; - beforeEach(function() { - callbackSpy.resetHistory(); + beforeEach(function () { }); - afterEach(function () { + afterEach(function () { + uspDataHandler.reset() }); it('should call the ID5 server and handle a valid response', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; - expect(requestBody.partner).to.eq(ID5_TEST_PARTNER_ID); - expect(requestBody.o).to.eq('pbjs'); - expect(requestBody.pd).to.be.undefined; - expect(requestBody.s).to.be.undefined; - expect(requestBody.provider).to.be.undefined - expect(requestBody.v).to.eq('$prebid.version$'); - expect(requestBody.gdpr).to.exist; - expect(requestBody.gdpr_consent).to.be.undefined; - expect(requestBody.us_privacy).to.be.undefined; - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let config = getId5FetchConfig(); + let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(fetchRequest.url).to.contain(ID5_ENDPOINT); + expect(fetchRequest.withCredentials).is.true; + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.pd).is.undefined; + expect(requestBody.s).is.undefined; + expect(requestBody.provider).is.undefined + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.gdpr).is.eq(0); + expect(requestBody.gdpr_consent).is.undefined; + expect(requestBody.us_privacy).is.undefined; + expect(requestBody.storage).is.deep.eq(config.storage) + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }); + + it('should call the ID5 server with gdpr data ', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let consentData = { + gdprApplies: true, + consentString: 'consentString' + } + + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.gdpr).to.eq(1); + expect(requestBody.gdpr_consent).is.eq(consentData.consentString); + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }); + + it('should call the ID5 server without gdpr data when gdpr not applies ', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let consentData = { + gdprApplies: false, + consentString: 'consentString' + } + + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.gdpr).to.eq(0); + expect(requestBody.gdpr_consent).is.undefined + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }); + + it('should call the ID5 server with us privacy consent', function () { + let usPrivacyString = '1YN-'; + uspDataHandler.setConsentData(usPrivacyString) + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let consentData = { + gdprApplies: true, + consentString: 'consentString' + } + + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.us_privacy).to.eq(usPrivacyString); + + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); }); it('should call the ID5 server with no signature field when no stored object', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; - submoduleCallback(callbackSpy); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.s).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.s).to.be.undefined; + it('should call the ID5 server for config with submodule config object', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let id5FetchConfig = getId5FetchConfig(); + id5FetchConfig.params.extraParam = { + x: 'X', + y: { + a: 1, + b: '3' + } + } + let submoduleResponse = callSubmoduleGetId(id5FetchConfig, undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + let requestBody = JSON.parse(configRequest.requestBody) + expect(requestBody).is.deep.eq(id5FetchConfig) + return xhrServerMock.respondWithConfigAndExpectNext(configRequest) + }) + .then(fetchRequest => { + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + it('should call the ID5 server for config with overridden url', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let id5FetchConfig = getId5FetchConfig(); + id5FetchConfig.params.configUrl = 'http://localhost/x/y/z' + + let submoduleResponse = callSubmoduleGetId(id5FetchConfig, undefined, undefined); + + return xhrServerMock.expectFirstRequest() + .then(configRequest => { + expect(configRequest.url).is.eq('http://localhost/x/y/z') + return xhrServerMock.respondWithConfigAndExpectNext(configRequest) + }) + .then(fetchRequest => { + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) }); - it('should call the ID5 server with signature field from stored object', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + it('should call the ID5 server with additional data when provided', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + return xhrServerMock.respondWithConfigAndExpectNext(configRequest, { + fetchCall: { + url: ID5_ENDPOINT, + overrides: { + arg1: '123', + arg2: { + x: '1', + y: 2 + } + } + } + }); + }) + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.arg1).is.eq('123') + expect(requestBody.arg2).is.deep.eq({ + x: '1', + y: 2 + }) + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); + it('should call the ID5 server with extensions', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + return xhrServerMock.respondWithConfigAndExpectNext(configRequest, { + fetchCall: { + url: ID5_ENDPOINT + }, + extensionsCall: { + url: ID5_EXTENSIONS_ENDPOINT, + method: 'GET' + } + }); + }) + .then(extensionsRequest => { + expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT) + expect(extensionsRequest.method).is.eq('GET') + extensionsRequest.respond(200, responseHeader, JSON.stringify({ + lb: 'ex' + })) + return xhrServerMock.expectNextRequest(); + }) + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.extensions).is.deep.eq({ + lb: 'ex' + }) + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); + + it('should call the ID5 server with extensions fetched with POST', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, undefined); + + return xhrServerMock.expectConfigRequest() + .then(configRequest => { + return xhrServerMock.respondWithConfigAndExpectNext(configRequest, { + fetchCall: { + url: ID5_ENDPOINT + }, + extensionsCall: { + url: ID5_EXTENSIONS_ENDPOINT, + method: 'POST', + body: { + x: '1', + y: 2 + } + } + }); + }) + .then(extensionsRequest => { + expect(extensionsRequest.url).is.eq(ID5_EXTENSIONS_ENDPOINT) + expect(extensionsRequest.method).is.eq('POST') + let requestBody = JSON.parse(extensionsRequest.requestBody) + expect(requestBody).is.deep.eq({ + x: '1', + y: 2 + }) + extensionsRequest.respond(200, responseHeader, JSON.stringify({ + lb: 'post', + })) + return xhrServerMock.expectNextRequest(); + }) + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.partner).is.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).is.eq('pbjs'); + expect(requestBody.v).is.eq('$prebid.version$'); + expect(requestBody.extensions).is.deep.eq({ + lb: 'post' + }) + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + }); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + it('should call the ID5 server with signature field from stored object', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) }); it('should call the ID5 server with pd field when pd config is set', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; let id5Config = getId5FetchConfig(); id5Config.params.pd = pubData; - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.pd).to.eq(pubData); + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.pd).is.eq(pubData); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }) }); it('should call the ID5 server with no pd field when pd config is not set', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); id5Config.params.pd = undefined; - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.pd).to.be.undefined; - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.pd).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }) }); it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.nbPage).to.eq(1); - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.nbPage).is.eq(1); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(() => { + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); + }) }); it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.nbPage).to.eq(2); - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.nbPage).is.eq(2); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(() => { + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); + }) }); it('should call the ID5 server with ab_testing object when abTesting is turned on', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); - id5Config.params.abTesting = { enabled: true, controlGroupPct: 0.234 } - - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + id5Config.params.abTesting = {enabled: true, controlGroupPct: 0.234} - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.ab_testing.enabled).to.eq(true); - expect(requestBody.ab_testing.control_group_pct).to.eq(0.234); + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.ab_testing.enabled).is.eq(true); + expect(requestBody.ab_testing.control_group_pct).is.eq(0.234); + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse; + }); }); it('should call the ID5 server without ab_testing object when abTesting is turned off', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); - id5Config.params.abTesting = { enabled: false, controlGroupPct: 0.55 } - - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + id5Config.params.abTesting = {enabled: false, controlGroupPct: 0.55} - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.ab_testing).to.be.undefined; + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.ab_testing).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }); }); it('should call the ID5 server without ab_testing when when abTesting is not set', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let id5Config = getId5FetchConfig(); - let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(requestBody.ab_testing).to.be.undefined; + let submoduleResponse = callSubmoduleGetId(id5Config, undefined, ID5_STORED_OBJ); - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.ab_testing).is.undefined; + fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }); }); it('should store the privacy object from the ID5 server response', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); - let request = server.requests[0]; - - let responseObject = utils.deepClone(ID5_JSON_RESPONSE); - responseObject.privacy = { + const privacy = { jurisdiction: 'gdpr', id5_consent: true }; - request.respond(200, responseHeader, JSON.stringify(responseObject)); - expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.eq(JSON.stringify(responseObject.privacy)); - coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); + + return xhrServerMock.expectFetchRequest() + .then(request => { + let responseObject = utils.deepClone(ID5_JSON_RESPONSE); + responseObject.privacy = privacy; + request.respond(200, responseHeader, JSON.stringify(responseObject)); + return submoduleResponse + }) + .then(() => { + expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).is.eq(JSON.stringify(privacy)); + coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); + }) }); it('should not store a privacy object if not part of ID5 server response', function () { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; - submoduleCallback(callbackSpy); - - let request = server.requests[0]; - - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.null; + let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ); + + return xhrServerMock.expectFetchRequest() + .then(request => { + let responseObject = utils.deepClone(ID5_JSON_RESPONSE); + responseObject.privacy = undefined; + request.respond(200, responseHeader, JSON.stringify(responseObject)); + return submoduleResponse + }) + .then(() => { + expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).is.null; + }); }); describe('when legacy cookies are set', () => { @@ -327,11 +674,11 @@ describe('ID5 ID System', function() { }) }); - describe('Request Bids Hook', function() { + describe('Request Bids Hook', function () { let adUnits; let sandbox; - beforeEach(function() { + beforeEach(function () { sandbox = sinon.sandbox.create(); mockGdprConsent(sandbox); sinon.stub(events, 'getEvents').returns([]); @@ -340,7 +687,7 @@ describe('ID5 ID System', function() { coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); adUnits = [getAdUnitMock()]; }); - afterEach(function() { + afterEach(function () { events.getEvents.restore(); coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); @@ -359,8 +706,8 @@ describe('ID5 ID System', function() { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[0]).to.deep.equal({ + expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).is.deep.equal({ source: ID5_SOURCE, uids: [{ id: ID5_STORED_ID, @@ -373,7 +720,7 @@ describe('ID5 ID System', function() { }); }); done(); - }, { adUnits }); + }, {adUnits}); }); it('should add config value ID to bids', function (done) { @@ -385,15 +732,15 @@ describe('ID5 ID System', function() { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); - expect(bid.userIdAsEids[0]).to.deep.equal({ + expect(bid.userId.id5id.uid).is.equal(ID5_STORED_ID); + expect(bid.userIdAsEids[0]).is.deep.equal({ source: ID5_SOURCE, - uids: [{ id: ID5_STORED_ID, atype: 1 }] + uids: [{id: ID5_STORED_ID, atype: 1}] }); }); }); done(); - }, { adUnits }); + }, {adUnits}); }); it('should set nb=1 in cache when no stored nb value exists and cached ID', function (done) { @@ -405,7 +752,7 @@ describe('ID5 ID System', function() { config.setConfig(getFetchLocalStorageConfig()); requestBidsHook((adUnitConfig) => { - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(1); done() }, {adUnits}); }); @@ -419,19 +766,20 @@ describe('ID5 ID System', function() { config.setConfig(getFetchLocalStorageConfig()); requestBidsHook(() => { - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); + expect(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 () { - storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let initialLocalStorageValue = JSON.stringify(ID5_STORED_OBJ); + storeInLocalStorage(ID5_STORAGE_NAME, initialLocalStorageValue, 1); storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); - storeNbInCache(ID5_TEST_PARTNER_ID, 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; - init(config); setSubmoduleRegistry([id5IdSubmodule]); config.setConfig(id5Config); @@ -441,53 +789,62 @@ describe('ID5 ID System', function() { resolve() }, {adUnits}); }).then(() => { - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); - expect(server.requests).to.be.empty; + expect(xhrServerMock.hasReceivedAnyRequest()).is.false; events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); - return new Promise((resolve) => setTimeout(resolve)) - }).then(() => { - let request = server.requests[0]; + return xhrServerMock.expectFetchRequest() + }).then(request => { let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); - expect(requestBody.nbPage).to.eq(2); - - const responseHeader = { 'Content-Type': 'application/json' }; + expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); + expect(requestBody.nbPage).is.eq(2); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(2); + const responseHeader = {'Content-Type': 'application/json'}; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); + return new Promise(function (resolve) { + (function waitForCondition() { + if (getFromLocalStorage(ID5_STORAGE_NAME) !== initialLocalStorageValue) return resolve(); + setTimeout(waitForCondition, 30); + })(); + }) + }).then(() => { + expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).is.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(0); }) }); }); - describe('Decode stored object', function() { - const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; + describe('Decode stored object', function () { + const expectedDecodedObject = {id5id: {uid: ID5_STORED_ID, ext: {linkType: ID5_STORED_LINK_TYPE}}}; - it('should properly decode from a stored object', function() { - expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject); + it('should properly decode from a stored object', function () { + expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).is.deep.equal(expectedDecodedObject); }); - it('should return undefined if passed a string', function() { - expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined); + it('should return undefined if passed a string', function () { + expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).is.eq(undefined); }); }); - describe('A/B Testing', function() { - const expectedDecodedObjectWithIdAbOff = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; - const expectedDecodedObjectWithIdAbOn = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false } } }; - const expectedDecodedObjectWithoutIdAbOn = { id5id: { uid: '', ext: { linkType: 0, abTestingControlGroup: true } } }; + describe('A/B Testing', function () { + const expectedDecodedObjectWithIdAbOff = {id5id: {uid: ID5_STORED_ID, ext: {linkType: ID5_STORED_LINK_TYPE}}}; + const expectedDecodedObjectWithIdAbOn = { + id5id: { + uid: ID5_STORED_ID, + ext: {linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false} + } + }; + const expectedDecodedObjectWithoutIdAbOn = {id5id: {uid: '', ext: {linkType: 0, abTestingControlGroup: true}}}; let testConfig, storedObject; - beforeEach(function() { + beforeEach(function () { testConfig = getId5FetchConfig(); storedObject = utils.deepClone(ID5_STORED_OBJ); }); - describe('A/B Testing Config is Set', function() { + describe('A/B Testing Config is Set', function () { let randStub; - beforeEach(function() { - randStub = sinon.stub(Math, 'random').callsFake(function() { + beforeEach(function () { + randStub = sinon.stub(Math, 'random').callsFake(function () { return 0.25; }); }); @@ -495,39 +852,39 @@ describe('ID5 ID System', function() { randStub.restore(); }); - describe('Decode', function() { + describe('Decode', function () { let logErrorSpy; - beforeEach(function() { + beforeEach(function () { logErrorSpy = sinon.spy(utils, 'logError'); }); - afterEach(function() { + afterEach(function () { logErrorSpy.restore(); }); it('should not set abTestingControlGroup extension when A/B testing is off', function () { let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOff); }); it('should set abTestingControlGroup to false when A/B testing is on but in normal group', function () { - storedObject.ab_testing = { result: 'normal' }; + storedObject.ab_testing = {result: 'normal'}; let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOn); }); it('should not expose ID when everyone is in control group', function () { - storedObject.ab_testing = { result: 'control' }; + storedObject.ab_testing = {result: 'control'}; storedObject.universal_uid = ''; storedObject.link_type = 0; let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + expect(decoded).is.deep.equal(expectedDecodedObjectWithoutIdAbOn); }); it('should log A/B testing errors', function () { - storedObject.ab_testing = { result: 'error' }; + storedObject.ab_testing = {result: 'error'}; let decoded = id5IdSubmodule.decode(storedObject, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + expect(decoded).is.deep.equal(expectedDecodedObjectWithIdAbOff); sinon.assert.calledOnce(logErrorSpy); }); }); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index a882f5ca5ef..19e91cbde96 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1031,7 +1031,7 @@ describe('Improve Digital Adapter Tests', function () { width: 728, height: 90, ttl: 300, - ad: '  ', + ad: '  ', creativeId: '510265', dealId: 320896, netRevenue: false, @@ -1042,6 +1042,12 @@ describe('Improve Digital Adapter Tests', function () { } ]; + const multiFormatExpectedBid = [ + Object.assign({}, expectedBid[0], { + ad: '  ' + }) + ]; + const expectedTwoBids = [ expectedBid[0], { @@ -1051,7 +1057,7 @@ describe('Improve Digital Adapter Tests', function () { width: 300, height: 250, ttl: 300, - ad: '  ', + ad: '  ', creativeId: '479163', dealId: 320896, netRevenue: false, @@ -1091,7 +1097,7 @@ describe('Improve Digital Adapter Tests', function () { it('should return a well-formed display bid for multi-format ad unit', function () { const bids = spec.interpretResponse(serverResponse, {bidderRequest: multiFormatBidderRequest}); - expect(bids).to.deep.equal(expectedBid); + expect(bids).to.deep.equal(multiFormatExpectedBid); }); it('should return two bids', function () { @@ -1233,16 +1239,6 @@ describe('Improve Digital Adapter Tests', function () { bids = spec.interpretResponse(videoResponse, {bidderRequest: multiFormatBidderRequest}); expect(bids[0].mediaType).to.equal(VIDEO); }); - - it('should not affect non-RAZR bids', function () { - const bids = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(bids[0].renderer).to.not.exist; - }); - - it('should detect RAZR bids', function () { - const bids = spec.interpretResponse(serverResponseRazr, {bidderRequest}); - expect(bids[0].renderer).to.exist; - }); }); describe('getUserSyncs', function () { diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 243b702f03d..7360024eed2 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -10,7 +10,6 @@ describe('IndexexchangeAdapter', function () { const IX_SECURE_ENDPOINT = 'https://htlb.casalemedia.com/openrtb/pbjs'; const VIDEO_ENDPOINT_VERSION = 8.1; const BANNER_ENDPOINT_VERSION = 7.2; - const NATIVE_ENDPOINT_VERSION = undefined; const SAMPLE_SCHAIN = { 'ver': '1.0', @@ -378,6 +377,7 @@ describe('IndexexchangeAdapter', function () { required: false }, title: { + len: 25, required: true }, body: { @@ -387,13 +387,20 @@ describe('IndexexchangeAdapter', function () { required: true }, video: { - required: false + required: false, + mimes: ['video/mp4', 'video/webm'], + minduration: 0, + maxduration: 120, + protocols: [2, 3, 5, 6] }, sponsoredBy: { required: true } } }, + nativeOrtbRequest: { + assets: [{id: 0, required: 0, img: {type: 1}}, {id: 1, required: 1, title: {len: 140}}, {id: 2, required: 1, data: {type: 2}}, {id: 3, required: 1, img: {type: 3}}, {id: 4, required: false, video: {mimes: ['video/mp4', 'video/webm'], minduration: 0, maxduration: 120, protocols: [2, 3, 5, 6]}}] + }, adUnitCode: 'div-gpt-ad-1460505748563-0', transactionId: '173f49a8-7549-4218-a23c-e7ba59b47231', bidId: '1a2b3c4f', @@ -432,6 +439,9 @@ describe('IndexexchangeAdapter', function () { } } }, + nativeOrtbRequest: { + assets: [{id: 0, required: 0, img: {type: 1}}, {id: 1, required: 1, title: {len: 140}}, {id: 2, required: 1, data: {type: 2}}, {id: 3, required: 1, img: {type: 3}}, {id: 4, required: false, video: {mimes: ['video/mp4', 'video/webm'], minduration: 0, maxduration: 120, protocols: [2, 3, 5, 6]}}] + }, adUnitCode: 'div-gpt-ad-1460505748562-0', transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', bidId: '1a2b3c4e', @@ -442,7 +452,7 @@ describe('IndexexchangeAdapter', function () { ]; const DEFAULT_NATIVE_IMP = { - request: '{"assets":[{"required":0,"img":{"type":1},"id":201},{"required":1,"title":{"len":25},"id":100},{"required":1,"data":{"type":2},"id":402},{"required":1,"img":{"type":3},"id":203},{"required":0,"video":{"mimes":["video/mp4","video/webm"],"minduration":0,"maxduration":120,"protocols":[2,3,5,6]},"id":300},{"required":1,"data":{"type":1},"id":401}],"ver":"1.2","eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', + request: '{"assets":[{"id":0,"required":0,"img":{"type":1}},{"id":1,"required":1,"title":{"len":140}},{"id":2,"required":1,"data":{"type":2}},{"id":3,"required":1,"img":{"type":3}},{"id":4,"required":false,"video":{"mimes":["video/mp4","video/webm"],"minduration":0,"maxduration":120,"protocols":[2,3,5,6]}}],"eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2' } @@ -588,7 +598,7 @@ describe('IndexexchangeAdapter', function () { advbrandid: 303325, advbrand: 'OECTA' }, - adm: '{"native":{"ver":"1.2","assets":[{"id":201,"img":{"url":"https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png","w":250,"h":250}},{"id":203,"img":{"url":"https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png","w":1200,"h":627}},{"id":401,"data":{"value":"autodoc.co.uk"}},{"id":402,"data":{"value":"Les pièces automobiles dont vous avez besoin, toujours sous la main."}},{"id":100,"title":{"text":"Autodoc"}},{"id":300,"video":{"vasttag":"blah"}}],"link":{"url":"https://play.google.com/store/apps/details?id=de.autodoc.gmbh","clicktrackers":["https://click.liftoff.io/v1/campaign_click/blah"]},"eventtrackers":[{"event":1,"method":1,"url":"https://impression-europe.liftoff.io/index/impression"},{"event":1,"method":1,"url":"https://a701.casalemedia.com/impression/v1"}],"privacy":"https://privacy.link.com"}}' + adm: '{"native":{"ver":"1.2","assets":[{"id":0,"img":{"url":"https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png","w":250,"h":250}},{"id":1,"img":{"url":"https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png","w":1200,"h":627}},{"id":2,"data":{"value":"autodoc.co.uk"}},{"id":3,"data":{"value":"Les pièces automobiles dont vous avez besoin, toujours sous la main."}},{"id":4,"title":{"text":"Autodoc"}},{"id":5,"video":{"vasttag":"blah"}}],"link":{"url":"https://play.google.com/store/apps/details?id=de.autodoc.gmbh","clicktrackers":["https://click.liftoff.io/v1/campaign_click/blah"]},"eventtrackers":[{"event":1,"method":1,"url":"https://impression-europe.liftoff.io/index/impression"},{"event":1,"method":1,"url":"https://a701.casalemedia.com/impression/v1"}],"privacy":"https://privacy.link.com"}}' } ], seat: '3970' @@ -864,7 +874,7 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('should return true when required params found for a banner or video ad', function () { + it('should return true when required params found for a banner, video or native ad', function () { expect(spec.isBidRequestValid(DEFAULT_BANNER_VALID_BID[0])).to.equal(true); expect(spec.isBidRequestValid(DEFAULT_VIDEO_VALID_BID[0])).to.equal(true); expect(spec.isBidRequestValid(DEFAULT_NATIVE_VALID_BID[0])).to.equal(true); @@ -1064,15 +1074,12 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should fail when native contains unrecongized properties', function () { + it('should fail if native openRTB object contains no valid assets', function () { let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID[0]); - bid.mediaTypes.native.test = {} + bid.nativeOrtbRequest = {} expect(spec.isBidRequestValid(bid)).to.be.false; - }); - it('should fail if native mediaTypes should contains no valid assets', function () { - let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID[0]); - bid.mediaTypes.native = {} + bid.nativeOrtbRequest = {assets: []} expect(spec.isBidRequestValid(bid)).to.be.false; }); }); @@ -2145,7 +2152,7 @@ describe('IndexexchangeAdapter', function () { it('should have native request', () => { const nativeImpression = JSON.parse(request[1].data.r).imp[0]; - expect(request[1].data.v).to.equal(NATIVE_ENDPOINT_VERSION); + expect(request[1].data.hasOwnProperty('v')).to.equal(false); expect(nativeImpression.id).to.equal(DEFAULT_NATIVE_VALID_BID[0].bidId); expect(nativeImpression.native).to.deep.equal(DEFAULT_NATIVE_IMP); }); @@ -2491,7 +2498,7 @@ describe('IndexexchangeAdapter', function () { const request = spec.buildRequests(DEFAULT_NATIVE_VALID_BID, DEFAULT_OPTION); const query = request[0].data; - expect(query.v).to.equal(NATIVE_ENDPOINT_VERSION); + expect(query.hasOwnProperty('v')).to.equal(false); expect(query.s).to.equal(DEFAULT_NATIVE_VALID_BID[0].params.siteId); expect(query.r).to.exist; expect(query.ac).to.equal('j'); @@ -2516,92 +2523,148 @@ describe('IndexexchangeAdapter', function () { const request = spec.buildRequests(DEFAULT_NATIVE_VALID_BID, DEFAULT_OPTION); const nativeImpression = JSON.parse(request[0].data.r).imp[0]; - expect(request[0].data.v).to.equal(NATIVE_ENDPOINT_VERSION); + expect(request[0].data.hasOwnProperty('v')).to.equal(false); expect(nativeImpression.id).to.equal(DEFAULT_NATIVE_VALID_BID[0].bidId); expect(nativeImpression.native).to.deep.equal(DEFAULT_NATIVE_IMP); }); it('should build request with given asset properties', function() { let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID) - bid[0].mediaTypes.native = { - title: { - len: 200 - }, - video: { - mimes: [ - 'javascript' - ], - minduration: 10, - maxduration: 60, - protocols: [1] - } + bid[0].nativeOrtbRequest = { + assets: [{id: 0, required: 0, title: {len: 140}}, {id: 1, required: 0, video: {mimes: ['javascript'], minduration: 10, maxduration: 60, protocols: [1]}}] } const request = spec.buildRequests(bid, DEFAULT_OPTION); const nativeImpression = JSON.parse(request[0].data.r).imp[0]; - expect(nativeImpression.native).to.deep.equal({request: '{"assets":[{"required":0,"title":{"len":200},"id":100},{"required":0,"video":{"mimes":["javascript"],"minduration":10,"maxduration":60,"protocols":[1]},"id":300}],"ver":"1.2","eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); + expect(nativeImpression.native).to.deep.equal({request: '{"assets":[{"id":0,"required":0,"title":{"len":140}},{"id":1,"required":0,"video":{"mimes":["javascript"],"minduration":10,"maxduration":60,"protocols":[1]}}],"eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); }); it('should build request with all possible Prebid asset properties', function() { let bid = utils.deepClone(DEFAULT_NATIVE_VALID_BID) - bid[0].mediaTypes.native = { - title: { - required: false - }, - body: { - required: false - }, - body2: { - required: false - }, - sponsoredBy: { - required: false - }, - icon: { - required: false - }, - image: { - required: false - }, - clickUrl: { - required: false - }, - displayUrl: { - required: false - }, - privacyLink: { - required: false - }, - privacyIcon: { - required: false - }, - cta: { - required: false - }, - rating: { - required: false - }, - downloads: { - required: false - }, - likes: { - required: false - }, - price: { - required: false - }, - salePrice: { - required: false - }, - address: { - required: false - }, - phone: { - required: false - }, + bid[0].nativeOrtbRequest = { + 'ver': '1.2', + 'assets': [ + { + 'id': 0, + 'required': 0, + 'title': { + 'len': 140 + } + }, + { + 'id': 1, + 'required': 0, + 'data': { + 'type': 2 + } + }, + { + 'id': 2, + 'required': 0, + 'data': { + 'type': 10 + } + }, + { + 'id': 3, + 'required': 0, + 'data': { + 'type': 1 + } + }, + { + 'id': 4, + 'required': 0, + 'img': { + 'type': 1 + } + }, + { + 'id': 5, + 'required': 0, + 'img': { + 'type': 3 + } + }, + { + 'id': 6, + 'required': 0 + }, + { + 'id': 7, + 'required': 0, + 'data': { + 'type': 11 + } + }, + { + 'id': 8, + 'required': 0 + }, + { + 'id': 9, + 'required': 0 + }, + { + 'id': 10, + 'required': 0, + 'data': { + 'type': 12 + } + }, + { + 'id': 11, + 'required': 0, + 'data': { + 'type': 3 + } + }, + { + 'id': 12, + 'required': 0, + 'data': { + 'type': 5 + } + }, + { + 'id': 13, + 'required': 0, + 'data': { + 'type': 4 + } + }, + { + 'id': 14, + 'required': 0, + 'data': { + 'type': 6 + } + }, + { + 'id': 15, + 'required': 0, + 'data': { + 'type': 7 + } + }, + { + 'id': 16, + 'required': 0, + 'data': { + 'type': 9 + } + }, + { + 'id': 17, + 'required': 0, + 'data': { + 'type': 8 + } + } + ] } const request = spec.buildRequests(bid, DEFAULT_OPTION); const nativeImpression = JSON.parse(request[0].data.r).imp[0]; - expect(nativeImpression.native).to.deep.equal({request: '{"assets":[{"required":0,"title":{"len":25},"id":100},{"required":0,"data":{"type":2},"id":402},{"required":0,"data":{"type":10},"id":410},{"required":0,"data":{"type":1},"id":401},{"required":0,"img":{"type":1},"id":201},{"required":0,"img":{"type":3},"id":203},{"required":0,"data":{"type":11},"id":411},{"required":0,"data":{"type":12},"id":412},{"required":0,"data":{"type":3},"id":403},{"required":0,"data":{"type":5},"id":405},{"required":0,"data":{"type":4},"id":404},{"required":0,"data":{"type":6},"id":406},{"required":0,"data":{"type":7},"id":407},{"required":0,"data":{"type":9},"id":409},{"required":0,"data":{"type":8},"id":408}],"ver":"1.2","eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); + expect(nativeImpression.native).to.deep.equal({request: '{"ver":"1.2","assets":[{"id":0,"required":0,"title":{"len":140}},{"id":1,"required":0,"data":{"type":2}},{"id":2,"required":0,"data":{"type":10}},{"id":3,"required":0,"data":{"type":1}},{"id":4,"required":0,"img":{"type":1}},{"id":5,"required":0,"img":{"type":3}},{"id":6,"required":0},{"id":7,"required":0,"data":{"type":11}},{"id":8,"required":0},{"id":9,"required":0},{"id":10,"required":0,"data":{"type":12}},{"id":11,"required":0,"data":{"type":3}},{"id":12,"required":0,"data":{"type":5}},{"id":13,"required":0,"data":{"type":4}},{"id":14,"required":0,"data":{"type":6}},{"id":15,"required":0,"data":{"type":7}},{"id":16,"required":0,"data":{"type":9}},{"id":17,"required":0,"data":{"type":8}}],"eventtrackers":[{"event":1,"methods":[1,2]}],"privacy":1}', ver: '1.2'}); }) }); @@ -3091,27 +3154,70 @@ describe('IndexexchangeAdapter', function () { advertiserDomains: ['www.abc.com'] }, native: { - body: 'Les pièces automobiles dont vous avez besoin, toujours sous la main.', - clickUrl: 'https://play.google.com/store/apps/details?id=de.autodoc.gmbh', - icon: { - height: 250, - width: 250, - url: 'https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png' - }, - image: { - height: 627, - width: 1200, - url: 'https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png' - }, - impressionTrackers: [ - 'https://click.liftoff.io/v1/campaign_click/blah', - 'https://impression-europe.liftoff.io/index/impression', - 'https://a701.casalemedia.com/impression/v1' - ], - privacyLink: 'https://privacy.link.com', - sponsoredBy: 'autodoc.co.uk', - title: 'Autodoc', - video: 'blah' + ortb: { + assets: [ + { + 'id': 0, + 'img': { + 'h': 250, + 'url': 'https://cdn.liftoff.io/customers/1209/creatives/2501-icon-250x250.png', + 'w': 250 + } + }, + { + 'id': 1, + 'img': { + 'h': 627, + 'url': 'https://cdn.liftoff.io/customers/5a9cab9cc6/image/lambda_png/a0355879b06c09b09232.png', + 'w': 1200 + } + }, + { + 'data': { + 'value': 'autodoc.co.uk' + }, + 'id': 2 + }, + { + 'data': { + 'value': 'Les pièces automobiles dont vous avez besoin, toujours sous la main.' + }, + 'id': 3 + }, + { + 'id': 4, + 'title': { + 'text': 'Autodoc' + } + }, + { + 'id': 5, + 'video': { + 'vasttag': 'blah' + } + } + ], + 'eventtrackers': [ + { + 'event': 1, + 'method': 1, + 'url': 'https://impression-europe.liftoff.io/index/impression' + }, + { + 'event': 1, + 'method': 1, + 'url': 'https://a701.casalemedia.com/impression/v1' + } + ], + 'link': { + 'clicktrackers': [ + 'https://click.liftoff.io/v1/campaign_click/blah' + ], + 'url': 'https://play.google.com/store/apps/details?id=de.autodoc.gmbh' + }, + 'privacy': 'https://privacy.link.com', + 'ver': '1.2' + } }, ttl: 3600 } diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 40005356fd8..303025888dc 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -104,9 +104,9 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); }); - it('Check if ORTB was added', function() { + it('Check if domain was added', function() { const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).site.domain).to.equal('example.com') + expect(JSON.parse(request[0].data).site.domain).to.exist }) it('Check if elOffsets was added', function() { @@ -114,11 +114,6 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).ext.nextMillennium.elOffsets).to.be.an('object') }) - it('Check if refferer was added', function() { - const request = spec.buildRequests(bidRequestData) - expect(JSON.parse(request[0].data).device.referrer).to.exist - }) - it('Check if imp object was added', function() { const request = spec.buildRequests(bidRequestData) expect(JSON.parse(request[0].data).imp).to.be.an('array') diff --git a/test/spec/modules/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index 324bf782672..8c378aaa416 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -58,6 +58,7 @@ describe('rasBidAdapter', function () { slot: 'test', area: 'areatest', site: 'test', + slotSequence: '0', network: '4178463' } }; @@ -140,6 +141,7 @@ describe('rasBidAdapter', function () { expect(requests[0].url).to.have.string('DV=test%2Fareatest'); expect(requests[0].url).to.have.string('kwrd=val1%2Bval2'); expect(requests[0].url).to.have.string('kvadunit=test%2Fareatest'); + expect(requests[0].url).to.have.string('pos0=0'); }); }); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index 19f417bd88f..08491edeb64 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -44,6 +44,106 @@ const bid = { }, }; +const ortbBid = { + adId: '123', + transactionId: 'au', + native: { + ortb: { + assets: [ + { + id: 0, + title: { + text: 'Native Creative' + } + }, + { + id: 1, + data: { + value: 'Cool description great stuff' + } + }, + { + id: 2, + data: { + value: 'Do it' + } + }, + { + id: 3, + img: { + url: 'http://cdn.example.com/p/creative-image/image.png', + h: 83, + w: 127 + } + }, + { + id: 4, + img: { + url: 'http://cdn.example.com/p/creative-image/icon.jpg', + h: 742, + w: 989 + } + }, + { + id: 5, + data: { + value: 'AppNexus', + type: 1 + } + } + ], + link: { + url: 'https://www.link.example' + }, + privacy: 'https://privacy-link.example', + ver: '1.2' + } + }, +}; + +const ortbRequest = { + assets: [ + { + id: 0, + required: 0, + title: { + len: 140 + } + }, { + id: 1, + required: 0, + data: { + type: 2 + } + }, { + id: 2, + required: 0, + data: { + type: 12 + } + }, { + id: 3, + required: 0, + img: { + type: 3 + } + }, { + id: 4, + required: 0, + img: { + type: 1 + } + }, { + id: 5, + required: 0, + data: { + type: 1 + } + } + ], + ver: '1.2' +} + const bidWithUndefinedFields = { transactionId: 'au', native: { @@ -401,6 +501,50 @@ describe('native.js', function () { }); }); + it('creates native all asset message with OpenRTB format', function () { + const messageRequest = { + message: 'Prebid Native', + action: 'allAssetRequest', + adId: '123', + }; + + const message = getAllAssetsMessage(messageRequest, ortbBid, {getNativeReq: () => ortbRequest}); + + expect(message.assets.length).to.equal(8); + expect(message.assets).to.deep.include({ + key: 'body', + value: bid.native.body, + }); + expect(message.assets).to.deep.include({ + key: 'image', + value: bid.native.image.url, + }); + expect(message.assets).to.deep.include({ + key: 'clickUrl', + value: bid.native.clickUrl, + }); + expect(message.assets).to.deep.include({ + key: 'title', + value: bid.native.title, + }); + expect(message.assets).to.deep.include({ + key: 'icon', + value: bid.native.icon.url, + }); + expect(message.assets).to.deep.include({ + key: 'cta', + value: bid.native.cta, + }); + expect(message.assets).to.deep.include({ + key: 'sponsoredBy', + value: bid.native.sponsoredBy, + }); + expect(message.assets).to.deep.include({ + key: 'privacyLink', + value: ortbBid.native.ortb.privacy, + }); + }); + const SAMPLE_ORTB_REQUEST = toOrtbNativeRequest({ title: 'vtitle', body: 'vbody'